Program Listing for File analysis_plugins.hpp#
↰ Return to documentation for file (src/analysis/analysis_plugins.hpp)
#pragma once
#include <QString>
#include <QVector>
namespace mosaic {
/// @brief The canonical registry of Analysis-tab plugins: id, display label,
/// one-line description and category, in one place.
///
/// This exists to make a specific class of bug impossible. The Analysis tab
/// used to hold two manually-maintained parallel sequences — a QComboBox's
/// addItem() calls and a QStackedWidget's addWidget() calls — coupled only by
/// the assumption that the Nth item and the Nth page belonged together.
/// Nothing enforced it, and getting it wrong showed the wrong plugin's
/// controls with no error at all. Identity is now the id string everywhere
/// (it already was for the run dispatch and the is_*_plugin() predicates);
/// pages are registered against that id rather than appended positionally.
///
/// Deliberately QtCore-only and header-only, matching pose_models.hpp: the
/// test target links Qt6::Core and Qt6::Network but not Qt6::Widgets, so
/// anything living in ui/ cannot be unit-tested. Keeping the registry here
/// is what lets tests/test_analysis_plugins.cpp guard it.
/// Groups the plugins by what they are *for*. The picker shows these as
/// headings, because a flat list of ten put data-prep utilities (frame-sync
/// repair, EEG/trigger alignment) beside real analyses with no visual
/// distinction between them.
enum class AnalysisPluginCategory {
BodyMotion,
FaceGaze,
Audio,
Privacy,
DataPrep,
};
struct AnalysisPluginDesc {
/// Stable identifier. Also AnalysisTabW::Impl::currentPlugin, the key for
/// run_analysis()'s dispatch, and the value compared by every
/// is_*_plugin() predicate. Never shown to the user; never reorder-
/// sensitive.
QString id;
/// Short enough to fit the picker without eliding at its default width.
QString label;
/// One line, shown under the label and as the row's tooltip.
QString blurb;
AnalysisPluginCategory category;
};
/// Every plugin, in the order the controls stack pages are built.
///
/// Order is preserved from the original combo purely so the stack's page
/// registration stays a straight one-line-per-plugin edit; nothing depends on
/// it any more. Display grouping is applied at render time from `category`.
///
/// The first entry is load-bearing: AnalysisTabW seeds its current plugin
/// from it at construction, and the tab's startup visibility pass only lands
/// correctly for a plugin whose visible widgets are the ones already visible
/// at construction time. That is Pose today. See AnalysisTabW's constructor.
[[nodiscard]] inline const QVector<AnalysisPluginDesc>& analysis_plugins() {
static const QVector<AnalysisPluginDesc> kPlugins = {
{"pose", "Pose", "Per-person 2D skeletons and keypoint kinematics.",
AnalysisPluginCategory::BodyMotion},
{"face_mask", "Face Masking", "Writes an anonymized copy; never touches the original.",
AnalysisPluginCategory::Privacy},
{"diarize", "Speaker Diarization", "Transcribes each mic and labels who is speaking.",
AnalysisPluginCategory::Audio},
{"expression", "Facial Expression", "Per-face emotion labels from facial landmarks.",
AnalysisPluginCategory::FaceGaze},
{"gaze_fusion", "Gaze Fusion", "Combines cameras into one 3D gaze ray. Needs calibration.",
AnalysisPluginCategory::FaceGaze},
{"pose3d", "3D Pose Reconstruction",
"Triangulates 2D poses into 3D. Needs Pose and calibration.",
AnalysisPluginCategory::BodyMotion},
{"trigger_sync", "EEG / Trigger Sync", "Maps trigger events onto each camera's frames.",
AnalysisPluginCategory::DataPrep},
// "experimental" stays in the label, not only the blurb: this
// plugin's whole safety framing depends on that caveat being
// impossible to miss (see Impl::rppgDisclaimerLbl).
{"rppg", "Heart Rate (experimental)", "Estimates pulse from facial colour change (rPPG).",
AnalysisPluginCategory::FaceGaze},
{"gaze2d", "2D Gaze", "Per-face gaze direction in image space. No calibration needed.",
AnalysisPluginCategory::FaceGaze},
{"sync_repair", "Frame Sync Repair", "Equalizes camera frame counts into synced/.",
AnalysisPluginCategory::DataPrep},
};
return kPlugins;
}
/// Categories in picker display order. A category missing here renders no
/// heading, and every plugin in it silently disappears from the picker — so
/// tests/test_analysis_plugins.cpp asserts this list and the categories
/// actually in use are the same set.
[[nodiscard]] inline QVector<AnalysisPluginCategory> analysis_plugin_categories() {
return {
AnalysisPluginCategory::BodyMotion, AnalysisPluginCategory::FaceGaze,
AnalysisPluginCategory::Audio, AnalysisPluginCategory::Privacy,
AnalysisPluginCategory::DataPrep,
};
}
[[nodiscard]] inline QString analysis_plugin_category_label(AnalysisPluginCategory category) {
switch (category) {
case AnalysisPluginCategory::BodyMotion:
return "Body & motion";
case AnalysisPluginCategory::FaceGaze:
return "Face & gaze";
case AnalysisPluginCategory::Audio:
return "Audio";
case AnalysisPluginCategory::Privacy:
return "Privacy";
case AnalysisPluginCategory::DataPrep:
return "Data prep & utilities";
}
return {};
}
/// Position of `id` in analysis_plugins(), or -1 if there is no such plugin.
/// -1 is a normal answer, not an error: an empty id (nothing selected yet) and
/// an unknown one both land here, and callers are expected to bail rather than
/// index with it.
[[nodiscard]] inline int analysis_plugin_index_of(const QString& id) {
const auto& plugins = analysis_plugins();
for (int i = 0; i < plugins.size(); ++i) {
if (plugins[i].id == id) {
return i;
}
}
return -1;
}
/// The registry entry for `id`, or nullptr if unknown.
[[nodiscard]] inline const AnalysisPluginDesc* analysis_plugin_for(const QString& id) {
const int index = analysis_plugin_index_of(id);
return index < 0 ? nullptr : &analysis_plugins()[index];
}
} // namespace mosaic