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/TimeRange.cpp
|
||||
src/Timeline.cpp
|
||||
src/Exporter.cpp
|
||||
)
|
||||
add_executable(rhubarb ${SOURCE_FILES})
|
||||
target_link_libraries(rhubarb ${Boost_LIBRARIES} cppFormat sphinxbase pocketSphinx)
|
||||
|
|
|
@ -63,7 +63,7 @@ public class EntryPoint {
|
|||
VideoTrack videoTrack = vegas.Project.AddVideoTrack();
|
||||
foreach (XmlElement mouthCueElement in mouthCueElements) {
|
||||
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);
|
||||
Media imageMedia = new Media(imageFileNames[mouthCueElement.InnerText]);
|
||||
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 <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
#include <boost/optional.hpp>
|
||||
#include <format.h>
|
||||
#include <tclap/CmdLine.h>
|
||||
|
@ -12,18 +10,18 @@
|
|||
#include "ProgressBar.h"
|
||||
#include "logging.h"
|
||||
#include <gsl_util.h>
|
||||
#include <tools.h>
|
||||
#include <Timeline.h>
|
||||
#include "Timeline.h"
|
||||
#include "Exporter.h"
|
||||
|
||||
using std::exception;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using std::unique_ptr;
|
||||
using std::make_unique;
|
||||
using std::map;
|
||||
using std::chrono::duration;
|
||||
using std::chrono::duration_cast;
|
||||
using boost::filesystem::path;
|
||||
using boost::property_tree::ptree;
|
||||
|
||||
namespace tclap = TCLAP;
|
||||
|
||||
|
@ -42,41 +40,20 @@ unique_ptr<AudioStream> createAudioStream(path filePath) {
|
|||
try {
|
||||
return std::make_unique<WaveFileReader>(filePath);
|
||||
} 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
|
||||
namespace TCLAP {
|
||||
template<>
|
||||
struct ArgTraits<LogLevel> {
|
||||
typedef ValueLike ValueCategory;
|
||||
};
|
||||
template<>
|
||||
struct ArgTraits<ExportFormat> {
|
||||
typedef ValueLike ValueCategory;
|
||||
};
|
||||
}
|
||||
|
||||
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<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);
|
||||
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);
|
||||
|
||||
try {
|
||||
|
@ -131,9 +111,22 @@ int main(int argc, char *argv[]) {
|
|||
|
||||
std::cerr << std::endl;
|
||||
|
||||
// Print XML
|
||||
ptree xmlTree = createXmlTree(inputFileName.getValue(), phones, shapes);
|
||||
boost::property_tree::write_xml(std::cout, xmlTree, boost::property_tree::xml_writer_settings<string>(' ', 2));
|
||||
// Export
|
||||
unique_ptr<Exporter> exporter;
|
||||
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;
|
||||
} catch (tclap::ArgException& e) {
|
||||
|
|
Loading…
Reference in New Issue