Merge pull request #21 from DanielSWolf/feature/structured

--machineReadable and --consoleLevel options
This commit is contained in:
Daniel Wolf 2017-09-13 20:49:24 +02:00 committed by GitHub
commit 22d8dbee64
15 changed files with 449 additions and 152 deletions

View File

@ -445,6 +445,10 @@ add_executable(rhubarb
src/rhubarb/main.cpp
src/rhubarb/ExportFormat.cpp
src/rhubarb/ExportFormat.h
src/rhubarb/semanticEntries.cpp
src/rhubarb/semanticEntries.h
src/rhubarb/sinks.cpp
src/rhubarb/sinks.h
)
target_include_directories(rhubarb PUBLIC "src/rhubarb")
target_link_libraries(rhubarb

View File

@ -27,6 +27,22 @@ Click the image for a demo video.
https://www.youtube.com/watch?v=zzdPSFJRlEo[image:http://img.youtube.com/vi/zzdPSFJRlEo/0.jpg[]]
== Integrations
[[afterEffects]]
=== Adobe After Effects
You can use Rhubarb Lip Sync to animate dialog right from Adobe After Effects. For more information, see the `extras` directory of the download.
image:img/after-effects.png[]
[[vegas]]
=== Magix Vegas
Rhubarb Lip Sync also comes with two plugin scripts for Magix Vegas (previously Sony Vegas). For more information, see the `extras` directory of the download.
image:img/vegas.png[]
[[mouth-shapes]]
== Mouth shapes
@ -88,21 +104,26 @@ Rhubarb Lip Sync is a command-line tool that is currently available for Windows
[[options]]
=== Command-line options ===
The following is a complete list of available command-line options.
==== Basic command-line options ====
The following command-line options are the most common:
[cols="2,5"]
|===
| Option | Description
| _<input file>_
| The audio file (.wav format) to be analyzed. This must be the last command-line argument.
| `-f` _<format>_, `--exportFormat` _<format>_
| The export format. Options: `tsv` (tab-separated values, see <<tsv,details>>), `xml` (see <<xml,details>>), `json` (see <<json,details>>).
_Default value: ``tsv``_
| `-d` _<path>_, `--dialogFile` _<path>_
| This option is meant for situations where you know the dialog text in advance. Specify a plain-text file (in ASCII or UTF-8 format) containing just the dialog of the audio file. Rhubarb Lip Sync will still perform word recognition internally, but it will prefer words and phrases that occur in the dialog file. This leads to better recognition results and thus more reliable animation.
| With this option, you can provide Rhubarb Lip Sync with the dialog text to get more reliable results. Specify the path to a plain-text file (in ASCII or UTF-8 format) containing the dialog contained in the audio file. Rhubarb Lip Sync will still perform word recognition internally, but it will prefer words and phrases that occur in the dialog file. This leads to better recognition results and thus more reliable animation.
For instance, let's say you're recording dialog for a computer game. The script says: "`That's all gobbledygook to me.`" But actually, the voice artist ends up saying "`That's _just_ gobbledygook to me,`" slightly changing the dialog. If you specify a dialog file with the original line ("`That's all gobbledygook to me`"), this will still allow Rhubarb Lip Sync to produce better results. Rhubarb Lip Sync will ignore the dialog file where it audibly differs from the recording, and benefit from it where it matches.
For instance, let's say you're recording dialog for a computer game. The script says: "`That's all gobbledygook to me.`" But actually, the voice artist ends up saying "`That's _just_ gobbledygook to me,`" deviating from the dialog. If you specify a dialog file with the original line ("`That's all gobbledygook to me`"), this will still allow Rhubarb Lip Sync to produce better results, because it will watch out for the uncommon word "`gobbledygook`". Rhubarb Lip Sync will ignore the dialog file where it audibly differs from the recording, and benefit from it where it matches.
_It is always a good idea to specify the dialog text. This will usually lead to more reliable mouth animation, even if the text is not completely accurate._
@ -112,24 +133,6 @@ _It is always a good idea to specify the dialog text. This will usually lead to
_Default value: ``GHX``_
| `--threads` _<number>_
| Rhubarb Lip Sync uses multithreading to speed up processing. By default, it creates as many worker threads as there are cores on your CPU, which results in optimal processing speed. You may choose to specify a lower number if you feel that Rhubarb Lip Sync is slowing down other applications. Specifying a higher number is not recommended, as it won't result in any additional speed-up.
Note that for short audio files, Rhubarb Lip Sync may choose to use fewer threads than specified.
_Default value: as many threads as your CPU has cores_
| `-q`, `--quiet`
| By default, Rhubarb Lip Sync writes a number of progress messages to `stderr`. If you're using it as part of a batch process, this may clutter your console. If you specify the `--quiet` flag, there won't be any output to `stderr` unless an error occurred.
| `--logFile` _<path>_
| Creates a log file with diagnostic information at the specified path.
|`--logLevel` _<level>_
| Sets the log level for the log file. Options: `trace`, `debug`, `info`, `warning`, `error`, `fatal`.
_Default value: ``debug``_
| `-o`, `--output` _<output file>_
| The name of the output file to create. If the file already exists, it will be overwritten. If you don't specify an output file, the result will be written to `stdout`.
@ -138,9 +141,84 @@ _Default value: ``debug``_
| `-h`, `--help`
| Displays usage information and exits.
|===
| _<input file>_
| The input file to be analyzed. Must be an sound file in WAVE format.
==== Advanced command-line options ====
The following command-line options can be helpful in special situations, especially when automating Rhubarb Lip Sync.
[cols="2,5"]
|===
| Option | Description
[[quiet]]
| `-q`, `--quiet`
| By default, Rhubarb Lip Sync writes a number of progress messages to `stderr`. If you're using it as part of a batch process, this may clutter your console. If you specify the `--quiet` flag, there won't be any output to `stderr` unless an error occurred.
You can combine this option with the <<consoleLevel,`consoleLevel`>> option to change the minimum event level that is printed to `stderr`.
| `--machineReadable`
a| This option is useful if you want to integrate Rhubarb Lip Sync with another (possibly graphical) application. All progress output to `stderr` will be in structured JSON format, allowing your program to parse the output and display a graphical progress bar or something similar.
In this mode, each line printed to `stderr` will be an object in JSON format. Every object contains the following:
* Property `type`: The type of the event. Currently, one of `"start"` (application start), `"progress"` (numeric progress), `"success"` (successful termination), `"failure"` (unsuccessful termination), and `"log"` (a log message without structured information).
* Event-specific structured data. For instance, a `"progress"` event contains the property `value` with a numeric value between 0.0 and 1.0.
* Property `log`: A log message describing the event, plus severity information. If you aren't interested in the structured data, you can display this as a fallback. For instance, a `"progress"` event with the structured information `"value": 0.69` may contain the following redundant log message: `"Progress: 69%"`.
You can combine this option with the <<consoleLevel,`consoleLevel`>> option. Note, however, that this only affects unstructured events of type `"log"` (not to be confused with the `log` property each event contains).
The following is an example output from a _successful_ run:
[source,json]
----
{ "type": "start", "file": "hi.wav", "log": { "level": "Info", "message": "Application startup. Input file: \"hi.wav\"." } }
{ "type": "progress", "value": 0.00, "log": { "level": "Trace", "message": "Progress: 0%" } }
{ "type": "progress", "value": 0.01, "log": { "level": "Trace", "message": "Progress: 1%" } }
{ "type": "progress", "value": 0.03, "log": { "level": "Trace", "message": "Progress: 3%" } }
{ "type": "progress", "value": 0.06, "log": { "level": "Trace", "message": "Progress: 6%" } }
{ "type": "progress", "value": 0.69, "log": { "level": "Trace", "message": "Progress: 68%" } }
{ "type": "progress", "value": 1.00, "log": { "level": "Trace", "message": "Progress: 100%" } }
{ "type": "success", "log": { "level": "Info", "message": "Application terminating normally." } }
----
The following is an example output from a _failed_ run:
[source,json]
----
{ "type": "start", "file": "no-such-file.wav", "log": { "level": "Info", "message": "Application startup. Input file: \"no-such-file.wav\"." } }
{ "type": "failure", "reason": "Error processing file \"no-such-file.wav\".\nCould not open sound file \"no-such-file.wav\".\nNo such file or directory", "log": { "level": "Fatal", "message": "Application terminating with error: Error processing file \"no-such-file.wav\".\nCould not open sound file \"no-such-file.wav\".\nNo such file or directory" } }
----
Note that the output format <<Versioning,adheres to SemVer>>. That means that the JSON output created after a minor upgrade will still be compatible. Note, however, that the following kinds of changes may occur at any time, because I interpret them as compatible:
* Additional types of progress events. Just ignore those events whose types you do not know or use their unstructured `log` property.
* Additional properties in any object. Just ignore properties you aren't interested in.
* Changes in JSON formatting, such as a re-ordering of properties or changes in whitespaces (except for line breaks -- every event will remain on a singe line.)
* Fewer or more events of type `"log"` or changes in the wording of log messages
[[consoleLevel]]
| `--consoleLevel` _<level>_
| Sets the log level for reporting to the console (`stderr`). Options: `trace`, `debug`, `info`, `warning`, `error`, `fatal`.
If <<quiet,`--quiet`>> is also specified, only events with the specified level or higher will be printed. Otherwise, a small number of essential events (startup, progress, etc.) will be printed even if their levels are below the specified value.
_Default value: ``error``_
| `--logFile` _<path>_
| Creates a log file with diagnostic information at the specified path.
|`--logLevel` _<level>_
| Sets the log level for the log file. Only events with the specified level or higher will be logged. Options: `trace`, `debug`, `info`, `warning`, `error`, `fatal`.
_Default value: ``debug``_
| `--threads` _<number>_
| Rhubarb Lip Sync uses multithreading to speed up processing. By default, it creates as many worker threads as there are cores on your CPU, which results in optimal processing speed. You may choose to specify a lower number if you feel that Rhubarb Lip Sync is slowing down other applications. Specifying a higher number is not recommended, as it won't result in any additional speed-up.
Note that for short audio files, Rhubarb Lip Sync may choose to use fewer threads than specified.
_Default value: as many threads as your CPU has cores_
|===
[[outputFormats]]
@ -220,37 +298,15 @@ JSON format is very similar to <<xml,XML format>>. The choice mainly depends on
There is nothing surprising here; everything said about XML format applies to JSON, too.
== Integrations
[[versioning]]
== Versioning (SemVer)
[[afterEffects]]
=== Adobe After Effects
Rhubarb Lip Sync uses Semantic Versioning (SemVer) for its command-line interface. For general information on Semantic Versioning, have a look at the http://semver.org/[official SemVer website].
You can use Rhubarb Lip Sync to animate dialog right from Adobe After Effects. For more information, see the `extras` directory of the download.
As a rule of thumb, everything you can use through the command-line interface adheres to SemVer. Everything else (i.e., the source code) does not. Here are some examples and clarifications:
image:img/after-effects.png[]
== I'd love to hear from you!
[[vegas]]
=== Magix Vegas
Have you created something great using Rhubarb Lip Sync? -- *https://twitter.com/RhubarbLipSync[Let me know on Twitter]* or *send me an email* at +++&#100;&#119;&#111;&#108;&#102;&#064;&#100;&#097;&#110;&#110;&#097;&#100;&#046;&#100;&#101;+++!
Rhubarb Lip Sync also comes with two plugin scripts for Magix Vegas (previously Sony Vegas). For more information, see the `extras` directory of the download.
image:img/vegas.png[]
== Limitations
Rhubarb Lip Sync has some limitations you should be aware of.
=== English only
Rhubarb Lip Sync only produces good results when you give it recordings in English. You'll get best results with American English.
=== 2D animation only
Rhubarb Lip Sync tries to imitate the animation style used in classic 2D animated cartoons. The results look stylized, and that's intentional. If you're working on a realistic 3D game or movie, Rhubarb Lip Sync may not be the best choice.
== Tell me what you think!
I'd love to hear from you!
* Have you created something great using Rhubarb Lip Sync? *https://twitter.com/RhubarbLipSync[Let me know on Twitter!]*
* Do you need help? Have you spotted a bug? Do you have a suggestion? *https://github.com/DanielSWolf/rhubarb-lip-sync/issues[Create an issue!]*
Do you need help? Have you spotted a bug? Do you have a suggestion? -- *https://github.com/DanielSWolf/rhubarb-lip-sync/issues[Create an issue!]*

View File

@ -3,6 +3,9 @@
## Unreleased
* Full Unicode support. File names, dialog files, strings in exported files etc. should now be fully Unicode-compatible.
* Added `--machineReadable` command-line option to allow for better integration with other applications.
* Added `--consoleLevel` command-line option to control how much detail to log to the console (`stderr`).
* Unless specified using `--consoleLevel`, only errors and fatal errors are printed to the console. Previously, warnings were also printed.
## Version 1.6.0
@ -76,7 +79,7 @@
Since version 1.0.0, Rhubarb Lip Sync can handle situations where the dialog text is specified (using the `-dialogFile` option), but the actual recording omits some words. For instance, the specified dialog text can be "That's all gobbledygook to me," but the recording only says "That's gobbledygook to me," dropping the word "all."
Until now, however, Rhubarb Lip Sync couldn't handle *changed* or *inserted* words, such as a recording saying "That's *just* gobbledygook to me." This restriction has been removed. As of version 1.2.0, the actual recording may freely deviate from the specified dialog text. Rhubarb Lip Sync will ignore the dialog file where it audibly differs from the recording, and benefit from it where it matches.
Until now, however, Rhubarb Lip Sync couldn't handle *changed* or *inserted* words, such as a recording saying "That's *just* gobbledygook to me." This restriction has been removed. As of version 1.2.0, the actual recording may freely deviate from the specified dialog text. Rhubarb Lip Sync will ignore the dialog file where it audibly differs from the recording, and benefit from it where it matches.
## Version 1.1.0

View File

@ -6,6 +6,7 @@ namespace logging {
struct Entry {
Entry(Level level, const std::string& message);
virtual ~Entry() = default;
time_t timestamp;
int threadCounter;

View File

@ -19,15 +19,37 @@ vector<shared_ptr<Sink>>& getSinks() {
return sinks;
}
void logging::addSink(shared_ptr<Sink> sink) {
bool logging::addSink(shared_ptr<Sink> sink) {
lock_guard<std::mutex> lock(getLogMutex());
getSinks().push_back(sink);
auto& sinks = getSinks();
if (std::find(sinks.begin(), sinks.end(), sink) == sinks.end()) {
sinks.push_back(sink);
return true;
}
return false;
}
void logging::log(Level level, const string& message) {
bool logging::removeSink(std::shared_ptr<Sink> sink) {
lock_guard<std::mutex> lock(getLogMutex());
auto& sinks = getSinks();
const auto it = std::find(sinks.begin(), sinks.end(), sink);
if (it != sinks.end()) {
sinks.erase(it);
return true;
}
return false;
}
void logging::log(const Entry& entry) {
lock_guard<std::mutex> lock(getLogMutex());
const Entry entry = Entry(level, message);
for (auto& sink : getSinks()) {
sink->receive(entry);
}
}
void logging::log(Level level, const string& message) {
const Entry entry = Entry(level, message);
log(entry);
}

View File

@ -6,7 +6,11 @@
namespace logging {
void addSink(std::shared_ptr<Sink> sink);
bool addSink(std::shared_ptr<Sink> sink);
bool removeSink(std::shared_ptr<Sink> sink);
void log(const Entry& entry);
void log(Level level, const std::string& message);

View File

@ -33,32 +33,4 @@ namespace logging {
StreamSink(std::shared_ptr<std::ostream>(&std::cerr, [](void*) {}), formatter)
{}
PausableSink::PausableSink(shared_ptr<Sink> innerSink) :
innerSink(innerSink)
{}
void PausableSink::receive(const Entry& entry) {
lock_guard<std::mutex> lock(mutex);
if (isPaused) {
buffer.push_back(entry);
} else {
innerSink->receive(entry);
}
}
void PausableSink::pause() {
lock_guard<std::mutex> lock(mutex);
isPaused = true;
}
void PausableSink::resume() {
lock_guard<std::mutex> lock(mutex);
isPaused = false;
for (const Entry& entry : buffer) {
innerSink->receive(entry);
}
buffer.clear();
}
}

View File

@ -3,7 +3,6 @@
#include "Sink.h"
#include <memory>
#include "Formatter.h"
#include <vector>
#include <mutex>
namespace logging {
@ -32,17 +31,4 @@ namespace logging {
explicit StdErrSink(std::shared_ptr<Formatter> formatter);
};
class PausableSink : public Sink {
public:
explicit PausableSink(std::shared_ptr<Sink> innerSink);
void receive(const Entry& entry) override;
void pause();
void resume();
private:
std::shared_ptr<Sink> innerSink;
std::vector<Entry> buffer;
std::mutex mutex;
bool isPaused = false;
};
}

View File

@ -22,11 +22,11 @@
#include "exporters/TsvExporter.h"
#include "exporters/XmlExporter.h"
#include "exporters/JsonExporter.h"
#include <boost/iostreams/stream.hpp>
#include <boost/iostreams/device/null.hpp>
#include "animation/targetShapeSet.h"
#include <boost/utility/in_place_factory.hpp>
#include "tools/platformTools.h"
#include "sinks.h"
#include "semanticEntries.h"
using std::exception;
using std::string;
@ -58,21 +58,12 @@ namespace TCLAP {
};
}
shared_ptr<logging::PausableSink> addPausableStdErrSink(logging::Level minLevel) {
auto stdErrSink = make_shared<logging::StdErrSink>(make_shared<logging::SimpleConsoleFormatter>());
auto pausableSink = make_shared<logging::PausableSink>(stdErrSink);
auto levelFilter = make_shared<logging::LevelFilter>(pausableSink, minLevel);
logging::addSink(levelFilter);
return pausableSink;
}
void addFileSink(path path, logging::Level minLevel) {
shared_ptr<logging::Sink> createFileSink(path path, logging::Level minLevel) {
auto file = make_shared<boost::filesystem::ofstream>();
file->exceptions(std::ifstream::failbit | std::ifstream::badbit);
file->open(path);
auto FileSink = make_shared<logging::StreamSink>(file, make_shared<logging::SimpleFileFormatter>());
auto levelFilter = make_shared<logging::LevelFilter>(FileSink, minLevel);
logging::addSink(levelFilter);
return make_shared<logging::LevelFilter>(FileSink, minLevel);
}
unique_ptr<Exporter> createExporter(ExportFormat exportFormat) {
@ -101,6 +92,11 @@ ShapeSet getTargetShapeSet(const string& extendedShapesString) {
}
int main(int platformArgc, char *platformArgv[]) {
// Set up default logging so early errors are printed to stdout
const logging::Level defaultMinStderrLevel = logging::Level::Error;
shared_ptr<logging::Sink> defaultSink = make_shared<NiceStderrSink>(defaultMinStderrLevel);
logging::addSink(defaultSink);
// Use UTF-8 throughout
useUtf8ForConsole();
useUtf8ForBoostFilesystem();
@ -108,9 +104,6 @@ int main(int platformArgc, char *platformArgv[]) {
// Convert command-line arguments to UTF-8
const vector<string> args = argsToUtf8(platformArgc, platformArgv);
auto pausableStderrSink = addPausableStdErrSink(logging::Level::Warn);
pausableStderrSink->pause();
// Define command-line parameters
const char argumentValueSeparator = ' ';
tclap::CmdLine cmd(appName, argumentValueSeparator, appVersion);
@ -119,9 +112,11 @@ int main(int platformArgc, char *platformArgv[]) {
tclap::ValueArg<string> outputFileName("o", "output", "The output file path.", false, string(), "string", cmd);
auto logLevels = vector<logging::Level>(logging::LevelConverter::get().getValues());
tclap::ValuesConstraint<logging::Level> logLevelConstraint(logLevels);
tclap::ValueArg<logging::Level> logLevel("", "logLevel", "The minimum log level to log", false, logging::Level::Debug, &logLevelConstraint, cmd);
tclap::ValueArg<logging::Level> logLevel("", "logLevel", "The minimum log level that will be written to the log file", false, logging::Level::Debug, &logLevelConstraint, cmd);
tclap::ValueArg<string> logFileName("", "logFile", "The log file path.", false, string(), "string", cmd);
tclap::SwitchArg quietMode("q", "quiet", "Suppresses all output to stderr except for error messages.", cmd, false);
tclap::ValueArg<logging::Level> consoleLevel("", "consoleLevel", "The minimum log level that will be printed on the console (stderr)", false, defaultMinStderrLevel, &logLevelConstraint, cmd);
tclap::SwitchArg machineReadableMode("", "machineReadable", "Formats all output to stderr in a structured JSON format.", cmd, false);
tclap::SwitchArg quietMode("q", "quiet", "Suppresses all output to stderr except for warnings and error messages.", cmd, false);
tclap::ValueArg<int> maxThreadCount("", "threads", "The maximum number of worker threads to use.", false, getProcessorCoreCount(), "number", cmd);
tclap::ValueArg<string> extendedShapes("", "extendedShapes", "All extended, optional shapes to use.", false, "GHX", "string", cmd);
tclap::ValueArg<string> dialogFile("d", "dialogFile", "A file containing the text of the dialog.", false, string(), "string", cmd);
@ -130,54 +125,52 @@ int main(int platformArgc, char *platformArgv[]) {
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);
std::ostream* infoStream = &std::cerr;
boost::iostreams::stream<boost::iostreams::null_sink> nullStream((boost::iostreams::null_sink()));
try {
auto resumeLogging = gsl::finally([&]() {
*infoStream << std::endl << std::endl;
pausableStderrSink->resume();
});
// Parse command line
{
// TCLAP mutates the function argument! Pass a copy.
vector<string> argsCopy(args);
cmd.parse(argsCopy);
}
// Set up logging
// ... to stderr
if (quietMode.getValue()) {
infoStream = &nullStream;
logging::addSink(make_shared<QuietStderrSink>(consoleLevel.getValue()));
} else if (machineReadableMode.getValue()) {
logging::addSink(make_shared<MachineReadableStderrSink>(consoleLevel.getValue()));
} else {
logging::addSink(make_shared<NiceStderrSink>(consoleLevel.getValue()));
}
logging::removeSink(defaultSink);
// ... to log file
if (logFileName.isSet()) {
auto fileSink = createFileSink(path(logFileName.getValue()), logLevel.getValue());
logging::addSink(fileSink);
}
// Validate and transform command line arguments
if (maxThreadCount.getValue() < 1) {
throw std::runtime_error("Thread count must be 1 or higher.");
}
path inputFilePath(inputFileName.getValue());
ShapeSet targetShapeSet = getTargetShapeSet(extendedShapes.getValue());
// Set up log file
if (logFileName.isSet()) {
addFileSink(path(logFileName.getValue()), logLevel.getValue());
}
logging::infoFormat("Application startup. Command line: {}", join(
args | transformed([](string arg) { return fmt::format("\"{}\"", arg); }), " "));
logging::log(StartEntry(inputFilePath));
logging::debugFormat("Command line: {}",
join(args | transformed([](string arg) { return fmt::format("\"{}\"", arg); }), " "));
try {
*infoStream << fmt::format("Generating lip sync data for {}.", inputFilePath) << std::endl;
*infoStream << "Processing. ";
JoiningContinuousTimeline<Shape> animation(TimeRange::zero(), Shape::X);
{
ProgressBar progressBar(*infoStream);
// On progress change: Create log message
ProgressForwarder progressSink([](double progress) { logging::log(ProgressEntry(progress)); });
// Animate the recording
animation = animateWaveFile(
inputFilePath,
dialogFile.isSet() ? readUtf8File(path(dialogFile.getValue())) : boost::optional<string>(),
targetShapeSet,
maxThreadCount.getValue(),
progressBar);
}
*infoStream << "Done." << std::endl << std::endl;
// Animate the recording
JoiningContinuousTimeline<Shape> animation = animateWaveFile(
inputFilePath,
dialogFile.isSet() ? readUtf8File(path(dialogFile.getValue())) : boost::optional<string>(),
targetShapeSet,
maxThreadCount.getValue(),
progressSink);
// Export animation
unique_ptr<Exporter> exporter = createExporter(exportFormat.getValue());
@ -189,7 +182,7 @@ int main(int platformArgc, char *platformArgv[]) {
ExporterInput exporterInput = ExporterInput(inputFilePath, animation, targetShapeSet);
exporter->exportAnimation(exporterInput, outputFile ? *outputFile : std::cout);
logging::info("Exiting application normally.");
logging::log(SuccessEntry());
} catch (...) {
std::throw_with_nested(std::runtime_error(fmt::format("Error processing file {}.", inputFilePath)));
}
@ -198,7 +191,7 @@ int main(int platformArgc, char *platformArgv[]) {
} catch (tclap::ArgException& e) {
// Error parsing command-line args.
cmd.getOutput()->failure(cmd, e);
logging::error("Invalid command line. Exiting application.");
logging::log(FailureEntry("Invalid command line."));
return 1;
} catch (tclap::ExitException&) {
// A built-in TCLAP command (like --help) has finished. Exit application.
@ -207,7 +200,7 @@ int main(int platformArgc, char *platformArgv[]) {
} catch (const exception& e) {
// Generic error
string message = getMessage(e);
logging::fatalFormat("Exiting application with error:\n{}", message);
logging::log(FailureEntry(message));
return 1;
}
}

View File

@ -0,0 +1,39 @@
#include "semanticEntries.h"
using logging::Level;
using std::string;
SemanticEntry::SemanticEntry(Level level, const string& message) :
Entry(level, message)
{}
StartEntry::StartEntry(const boost::filesystem::path& inputFilePath) :
SemanticEntry(Level::Info, fmt::format("Application startup. Input file: {}.", inputFilePath)),
inputFilePath(inputFilePath)
{}
boost::filesystem::path StartEntry::getInputFilePath() const {
return inputFilePath;
}
ProgressEntry::ProgressEntry(double progress) :
SemanticEntry(Level::Trace, fmt::format("Progress: {}%", static_cast<int>(progress * 100))),
progress(progress)
{}
double ProgressEntry::getProgress() const {
return progress;
}
SuccessEntry::SuccessEntry() :
SemanticEntry(Level::Info, "Application terminating normally.")
{}
FailureEntry::FailureEntry(const string& reason) :
SemanticEntry(Level::Fatal, fmt::format("Application terminating with error: {}", reason)),
reason(reason)
{}
string FailureEntry::getReason() const {
return reason;
}

View File

@ -0,0 +1,38 @@
#pragma once
#include "logging/Entry.h"
#include <boost/filesystem/path.hpp>
// Marker class for semantic entries
class SemanticEntry : public logging::Entry {
public:
SemanticEntry(logging::Level level, const std::string& message);
};
class StartEntry : public SemanticEntry {
public:
StartEntry(const boost::filesystem::path& inputFilePath);
boost::filesystem::path getInputFilePath() const;
private:
boost::filesystem::path inputFilePath;
};
class ProgressEntry : public SemanticEntry {
public:
ProgressEntry(double progress);
double getProgress() const;
private:
double progress;
};
class SuccessEntry : public SemanticEntry {
public:
SuccessEntry();
};
class FailureEntry : public SemanticEntry {
public:
FailureEntry(const std::string& reason);
std::string getReason() const;
private:
std::string reason;
};

122
src/rhubarb/sinks.cpp Normal file
View File

@ -0,0 +1,122 @@
#include "sinks.h"
#include "logging/sinks.h"
#include "logging/formatters.h"
#include "semanticEntries.h"
#include "tools/stringTools.h"
#include "core/appInfo.h"
#include <boost/utility/in_place_factory.hpp>
using std::string;
using std::make_shared;
using logging::Level;
using logging::LevelFilter;
using logging::StdErrSink;
using logging::SimpleConsoleFormatter;
using boost::optional;
NiceStderrSink::NiceStderrSink(Level minLevel) :
minLevel(minLevel),
innerSink(make_shared<StdErrSink>(make_shared<SimpleConsoleFormatter>()))
{}
void NiceStderrSink::receive(const logging::Entry& entry) {
// For selected semantic entries, print a user-friendly message instead of the technical log message.
if (const StartEntry* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
std::cerr << fmt::format("Generating lip sync data for {}.", startEntry->getInputFilePath()) << std::endl;
startProgressIndication();
} else if (const ProgressEntry* progressEntry = dynamic_cast<const ProgressEntry*>(&entry)) {
assert(progressBar);
progressBar->reportProgress(progressEntry->getProgress());
} else if (dynamic_cast<const SuccessEntry*>(&entry)) {
interruptProgressIndication();
std::cerr << "Done." << std::endl;
} else {
// Treat the entry as a normal log message
if (entry.level >= minLevel) {
const bool inProgress = progressBar.is_initialized();
if (inProgress) interruptProgressIndication();
innerSink->receive(entry);
if (inProgress) resumeProgressIndication();
}
}
}
void NiceStderrSink::startProgressIndication() {
std::cerr << "Progress: ";
progressBar = boost::in_place();
progressBar->setClearOnDestruction(false);
}
void NiceStderrSink::interruptProgressIndication() {
progressBar.reset();
std::cerr << std::endl;
}
void NiceStderrSink::resumeProgressIndication() {
std::cerr << "Progress (cont'd): ";
progressBar = boost::in_place();
progressBar->setClearOnDestruction(false);
}
QuietStderrSink::QuietStderrSink(Level minLevel) :
minLevel(minLevel),
innerSink(make_shared<StdErrSink>(make_shared<SimpleConsoleFormatter>()))
{}
void QuietStderrSink::receive(const logging::Entry& entry) {
// Set inputFilePath as soon as we get it
if (const StartEntry* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
inputFilePath = startEntry->getInputFilePath();
}
if (entry.level >= minLevel) {
if (quietSoFar) {
// This is the first message we print. Give a bit of context.
const string intro = inputFilePath
? fmt::format("{} {} processing file {}:", appName, appVersion, *inputFilePath)
: fmt::format("{} {}:", appName, appVersion);
std::cerr << intro << std::endl;
quietSoFar = false;
}
innerSink->receive(entry);
}
}
MachineReadableStderrSink::MachineReadableStderrSink(Level minLevel) :
minLevel(minLevel)
{}
string formatLogProperty(const logging::Entry& entry) {
return fmt::format(R"("log": {{ "level": "{}", "message": "{}" }})", entry.level, escapeJsonString(entry.message));
}
void MachineReadableStderrSink::receive(const logging::Entry& entry) {
optional<string> line;
if (dynamic_cast<const SemanticEntry*>(&entry)) {
if (const StartEntry* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
const string file = escapeJsonString(startEntry->getInputFilePath().string());
line = fmt::format(R"({{ "type": "start", "file": "{}", {} }})", file, formatLogProperty(entry));
} else if (const ProgressEntry* progressEntry = dynamic_cast<const ProgressEntry*>(&entry)) {
const int progressPercent = static_cast<int>(progressEntry->getProgress() * 100);
if (progressPercent > lastProgressPercent) {
line = fmt::format(R"({{ "type": "progress", "value": {:.2f}, {} }})", progressEntry->getProgress(), formatLogProperty(entry));
lastProgressPercent = progressPercent;
}
} else if (dynamic_cast<const SuccessEntry*>(&entry)) {
line = fmt::format(R"({{ "type": "success", {} }})", formatLogProperty(entry));
} else if (const FailureEntry* failureEntry = dynamic_cast<const FailureEntry*>(&entry)) {
const string reason = escapeJsonString(failureEntry->getReason());
line = fmt::format(R"({{ "type": "failure", "reason": "{}", {} }})", reason, formatLogProperty(entry));
} else {
throw std::runtime_error("Unsupported type of semantic entry.");
}
} else {
if (entry.level >= minLevel) {
line = fmt::format(R"({{ "type": "log", {} }})", formatLogProperty(entry));
}
}
if (line) {
std::cerr << *line << std::endl;
}
}

46
src/rhubarb/sinks.h Normal file
View File

@ -0,0 +1,46 @@
#pragma once
#include "logging/Entry.h"
#include "logging/Sink.h"
#include "tools/ProgressBar.h"
#include <boost/filesystem/path.hpp>
// Prints nicely formatted progress to stderr.
// Non-semantic entries are only printed if their log level at least matches the specified minimum level.
class NiceStderrSink : public logging::Sink {
public:
NiceStderrSink(logging::Level minLevel);
void receive(const logging::Entry& entry) override;
private:
void startProgressIndication();
void interruptProgressIndication();
void resumeProgressIndication();
logging::Level minLevel;
boost::optional<ProgressBar> progressBar;
std::shared_ptr<Sink> innerSink;
};
// Mostly quiet output to stderr.
// Entries are only printed if their log level at least matches the specified minimum level.
class QuietStderrSink : public logging::Sink {
public:
QuietStderrSink(logging::Level minLevel);
void receive(const logging::Entry& entry) override;
private:
logging::Level minLevel;
bool quietSoFar = true;
boost::optional<boost::filesystem::path> inputFilePath;
std::shared_ptr<Sink> innerSink;
};
// Prints machine-readable progress to stderr.
// Non-semantic entries are only printed if their log level at least matches the specified minimum level.
class MachineReadableStderrSink : public logging::Sink {
public:
MachineReadableStderrSink(logging::Level minLevel);
void receive(const logging::Entry& entry) override;
private:
logging::Level minLevel;
int lastProgressPercent = -1;
};

View File

@ -87,7 +87,9 @@ void ProgressBar::updateLoop() {
std::this_thread::sleep_for(animationInterval);
}
updateText("");
if (clearOnDestruction) {
updateText("");
}
}
void ProgressBar::updateText(const string& text) {

View File

@ -48,6 +48,14 @@ public:
~ProgressBar();
void reportProgress(double value) override;
bool getClearOnDestruction() const {
return clearOnDestruction;
}
void setClearOnDestruction(bool value) {
clearOnDestruction = value;
}
private:
void updateLoop();
void updateText(const std::string& text);
@ -59,4 +67,5 @@ private:
std::ostream& stream;
std::string currentText;
int animationIndex = 0;
bool clearOnDestruction = true;
};