Supporting multiple export formats
- Simplified XML export format - Added TSV and JSON formats - Using TSV as standard export format
This commit is contained in:
parent
90e1375f1b
commit
fd6b3b1e2f
|
@ -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)
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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";
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
61
src/main.cpp
61
src/main.cpp
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue