Supporting multiple export formats

- Simplified XML export format
- Added TSV and JSON formats
- Using TSV as standard export format
This commit is contained in:
Daniel Wolf 2016-04-12 20:49:06 +02:00
parent 90e1375f1b
commit fd6b3b1e2f
5 changed files with 194 additions and 35 deletions

View File

@ -121,6 +121,7 @@ set(SOURCE_FILES
src/Timed.cpp src/Timed.cpp
src/TimeRange.cpp src/TimeRange.cpp
src/Timeline.cpp src/Timeline.cpp
src/Exporter.cpp
) )
add_executable(rhubarb ${SOURCE_FILES}) add_executable(rhubarb ${SOURCE_FILES})
target_link_libraries(rhubarb ${Boost_LIBRARIES} cppFormat sphinxbase pocketSphinx) target_link_libraries(rhubarb ${Boost_LIBRARIES} cppFormat sphinxbase pocketSphinx)

View File

@ -63,7 +63,7 @@ public class EntryPoint {
VideoTrack videoTrack = vegas.Project.AddVideoTrack(); VideoTrack videoTrack = vegas.Project.AddVideoTrack();
foreach (XmlElement mouthCueElement in mouthCueElements) { foreach (XmlElement mouthCueElement in mouthCueElements) {
Timecode start = GetTimecode(mouthCueElement.Attributes["start"]); Timecode start = GetTimecode(mouthCueElement.Attributes["start"]);
Timecode length = GetTimecode(mouthCueElement.Attributes["duration"]); Timecode length = GetTimecode(mouthCueElement.Attributes["end"]) - start;
VideoEvent videoEvent = videoTrack.AddVideoEvent(start, length); VideoEvent videoEvent = videoTrack.AddVideoEvent(start, length);
Media imageMedia = new Media(imageFileNames[mouthCueElement.InnerText]); Media imageMedia = new Media(imageFileNames[mouthCueElement.InnerText]);
videoEvent.AddTake(imageMedia.GetVideoStreamByIndex(0)); videoEvent.AddTake(imageMedia.GetVideoStreamByIndex(0));

122
src/Exporter.cpp Normal file
View File

@ -0,0 +1,122 @@
#include "Exporter.h"
#include <logging.h>
#include <vector>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
using std::string;
using boost::property_tree::ptree;
using std::vector;
using std::tuple;
using std::make_tuple;
template <>
const string& getEnumTypeName<ExportFormat>() {
static const string name = "ExportFormat";
return name;
}
template <>
const vector<tuple<ExportFormat, string>>& getEnumMembers<ExportFormat>() {
static const vector<tuple<ExportFormat, string>> values = {
make_tuple(ExportFormat::TSV, "TSV"),
make_tuple(ExportFormat::XML, "XML"),
make_tuple(ExportFormat::JSON, "JSON")
};
return values;
}
std::ostream& operator<<(std::ostream& stream, ExportFormat value) {
return stream << enumToString(value);
}
std::istream& operator>>(std::istream& stream, ExportFormat& value) {
string name;
stream >> name;
value = parseEnum<ExportFormat>(name);
return stream;
}
// Makes sure there is at least one mouth shape
std::vector<Timed<Shape>> dummyShapeIfEmpty(const Timeline<Shape>& shapes) {
std::vector<Timed<Shape>> result;
std::copy(shapes.begin(), shapes.end(), std::back_inserter(result));
if (result.empty()) {
// Add zero-length empty mouth
result.push_back(Timed<Shape>(centiseconds(0), centiseconds(0), Shape::A));
}
return result;
}
void TSVExporter::exportShapes(const boost::filesystem::path& inputFilePath, const Timeline<Shape>& shapes, std::ostream& outputStream) {
UNUSED(inputFilePath);
// Output shapes with start times
for (auto& timedShape : shapes) {
outputStream << formatDuration(timedShape.getStart()) << "\t" << timedShape.getValue() << "\n";
}
// Output closed mouth with end time
outputStream << formatDuration(shapes.getRange().getEnd()) << "\t" << Shape::A << "\n";
}
void XMLExporter::exportShapes(const boost::filesystem::path& inputFilePath, const Timeline<Shape>& shapes, std::ostream& outputStream) {
ptree tree;
// Add metadata
tree.put("rhubarbResult.metadata.soundFile", inputFilePath.string());
tree.put("rhubarbResult.metadata.duration", formatDuration(shapes.getRange().getLength()));
// Add mouth cues
for (auto& timedShape : dummyShapeIfEmpty(shapes)) {
ptree& mouthCueElement = tree.add("rhubarbResult.mouthCues.mouthCue", timedShape.getValue());
mouthCueElement.put("<xmlattr>.start", formatDuration(timedShape.getStart()));
mouthCueElement.put("<xmlattr>.end", formatDuration(timedShape.getEnd()));
}
write_xml(outputStream, tree, boost::property_tree::xml_writer_settings<string>(' ', 2));
}
string escapeJSONString(const string& s) {
string result;
for (char c : s) {
switch (c) {
case '"': result += "\\\""; break;
case '\\': result += "\\\\"; break;
case '\b': result += "\\b"; break;
case '\f': result += "\\f"; break;
case '\n': result += "\\n"; break;
case '\r': result += "\\r"; break;
case '\t': result += "\\t"; break;
default:
if (c <= '\x1f') {
result += fmt::format("\\u{0:04x}", c);
} else {
result += c;
}
}
}
return result;
}
void JSONExporter::exportShapes(const boost::filesystem::path& inputFilePath, const Timeline<Shape>& shapes, std::ostream& outputStream) {
// Export as JSON.
// I'm not using a library because the code is short enough without one and it lets me control the formatting.
outputStream << "{\n";
outputStream << " \"metadata\": {\n";
outputStream << " \"soundFile\": \"" << escapeJSONString(inputFilePath.string()) << "\",\n";
outputStream << " \"duration\": " << formatDuration(shapes.getRange().getLength()) << "\n";
outputStream << " },\n";
outputStream << " \"mouthCues\": [\n";
bool isFirst = true;
for (auto& timedShape : dummyShapeIfEmpty(shapes)) {
if (!isFirst) outputStream << ",\n";
isFirst = false;
outputStream << " { \"start\": " << formatDuration(timedShape.getStart())
<< ", \"end\": " << formatDuration(timedShape.getEnd())
<< ", \"value\": \"" << timedShape.getValue() << "\" }";
}
outputStream << "\n";
outputStream << " ]\n";
outputStream << "}\n";
}

43
src/Exporter.h Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include <Shape.h>
#include <Timeline.h>
#include <boost/filesystem/path.hpp>
#include <iostream>
enum class ExportFormat {
TSV,
XML,
JSON
};
template<>
const std::string& getEnumTypeName<ExportFormat>();
template<>
const std::vector<std::tuple<ExportFormat, std::string>>& getEnumMembers<ExportFormat>();
std::ostream& operator<<(std::ostream& stream, ExportFormat value);
std::istream& operator>>(std::istream& stream, ExportFormat& value);
class Exporter {
public:
virtual ~Exporter() {}
virtual void exportShapes(const boost::filesystem::path& inputFilePath, const Timeline<Shape>& shapes, std::ostream& outputStream) = 0;
};
class TSVExporter : public Exporter {
public:
void exportShapes(const boost::filesystem::path& inputFilePath, const Timeline<Shape>& shapes, std::ostream& outputStream) override;
};
class XMLExporter : public Exporter {
public:
void exportShapes(const boost::filesystem::path& inputFilePath, const Timeline<Shape>& shapes, std::ostream& outputStream) override;
};
class JSONExporter : public Exporter {
public:
void exportShapes(const boost::filesystem::path& inputFilePath, const Timeline<Shape>& shapes, std::ostream& outputStream) override;
};

View File

@ -1,6 +1,4 @@
#include <iostream> #include <iostream>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/optional.hpp> #include <boost/optional.hpp>
#include <format.h> #include <format.h>
#include <tclap/CmdLine.h> #include <tclap/CmdLine.h>
@ -12,18 +10,18 @@
#include "ProgressBar.h" #include "ProgressBar.h"
#include "logging.h" #include "logging.h"
#include <gsl_util.h> #include <gsl_util.h>
#include <tools.h> #include "Timeline.h"
#include <Timeline.h> #include "Exporter.h"
using std::exception; using std::exception;
using std::string; using std::string;
using std::vector; using std::vector;
using std::unique_ptr; using std::unique_ptr;
using std::make_unique;
using std::map; using std::map;
using std::chrono::duration; using std::chrono::duration;
using std::chrono::duration_cast; using std::chrono::duration_cast;
using boost::filesystem::path; using boost::filesystem::path;
using boost::property_tree::ptree;
namespace tclap = TCLAP; namespace tclap = TCLAP;
@ -42,41 +40,20 @@ unique_ptr<AudioStream> createAudioStream(path filePath) {
try { try {
return std::make_unique<WaveFileReader>(filePath); return std::make_unique<WaveFileReader>(filePath);
} catch (...) { } catch (...) {
std::throw_with_nested(std::runtime_error(fmt::format("Could not open sound file {0}.", filePath))); std::throw_with_nested(std::runtime_error(fmt::format("Could not open sound file '{0}'.", filePath.string())));
} }
} }
ptree createXmlTree(const path& filePath, const Timeline<Phone>& phones, const Timeline<Shape>& shapes) {
ptree tree;
// Add sound file path
tree.put("rhubarbResult.info.soundFile", filePath.string());
// Add phones
tree.put("rhubarbResult.phones", "");
for (auto& timedPhone : phones) {
ptree& phoneElement = tree.add("rhubarbResult.phones.phone", timedPhone.getValue());
phoneElement.put("<xmlattr>.start", formatDuration(timedPhone.getStart()));
phoneElement.put("<xmlattr>.duration", formatDuration(timedPhone.getLength()));
}
// Add mouth cues
tree.put("rhubarbResult.mouthCues", "");
for (auto& timedShape : shapes) {
ptree& mouthCueElement = tree.add("rhubarbResult.mouthCues.mouthCue", timedShape.getValue());
mouthCueElement.put("<xmlattr>.start", formatDuration(timedShape.getStart()));
mouthCueElement.put("<xmlattr>.duration", formatDuration(timedShape.getLength()));
}
return tree;
}
// Tell TCLAP how to handle our types // Tell TCLAP how to handle our types
namespace TCLAP { namespace TCLAP {
template<> template<>
struct ArgTraits<LogLevel> { struct ArgTraits<LogLevel> {
typedef ValueLike ValueCategory; typedef ValueLike ValueCategory;
}; };
template<>
struct ArgTraits<ExportFormat> {
typedef ValueLike ValueCategory;
};
} }
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
@ -93,6 +70,9 @@ int main(int argc, char *argv[]) {
tclap::ValueArg<LogLevel> logLevel("", "logLevel", "The minimum log level to log", false, LogLevel::Debug, &logLevelConstraint, cmd); tclap::ValueArg<LogLevel> logLevel("", "logLevel", "The minimum log level to log", false, LogLevel::Debug, &logLevelConstraint, cmd);
tclap::ValueArg<string> logFileName("", "logFile", "The log file path.", false, string(), "string", cmd); tclap::ValueArg<string> logFileName("", "logFile", "The log file path.", false, string(), "string", cmd);
tclap::ValueArg<string> dialog("d", "dialog", "The text of the dialog.", false, string(), "string", cmd); tclap::ValueArg<string> dialog("d", "dialog", "The text of the dialog.", false, string(), "string", cmd);
auto exportFormats = vector<ExportFormat>(getEnumValues<ExportFormat>());
tclap::ValuesConstraint<ExportFormat> exportFormatConstraint(exportFormats);
tclap::ValueArg<ExportFormat> exportFormat("f", "exportFormat", "The export format.", false, ExportFormat::TSV, &exportFormatConstraint, cmd);
tclap::UnlabeledValueArg<string> inputFileName("inputFile", "The input file. Must be a sound file in WAVE format.", true, "", "string", cmd); tclap::UnlabeledValueArg<string> inputFileName("inputFile", "The input file. Must be a sound file in WAVE format.", true, "", "string", cmd);
try { try {
@ -131,9 +111,22 @@ int main(int argc, char *argv[]) {
std::cerr << std::endl; std::cerr << std::endl;
// Print XML // Export
ptree xmlTree = createXmlTree(inputFileName.getValue(), phones, shapes); unique_ptr<Exporter> exporter;
boost::property_tree::write_xml(std::cout, xmlTree, boost::property_tree::xml_writer_settings<string>(' ', 2)); switch (exportFormat.getValue()) {
case ExportFormat::TSV:
exporter = make_unique<TSVExporter>();
break;
case ExportFormat::XML:
exporter = make_unique<XMLExporter>();
break;
case ExportFormat::JSON:
exporter = make_unique<JSONExporter>();
break;
default:
throw std::runtime_error("Unknown export format.");
}
exporter->exportShapes(path(inputFileName.getValue()), shapes, std::cout);
return 0; return 0;
} catch (tclap::ArgException& e) { } catch (tclap::ArgException& e) {