From 97ef62afee117ce515f398f179ee0b46fcd40746 Mon Sep 17 00:00:00 2001 From: beckett Date: Wed, 19 Aug 2026 12:58:01 -0500 Subject: [PATCH] Gray out Train Aligners until a dataset has manual alignments Training only produces a meaningful model when it can learn from a hand-corrected alignment; training against machine-generated alignments alone just reinforces their own errors. Disable the sidebar step and its page whenever no registered dataset has a manual/hand alignment, and re-evaluate on reload so registering one enables it without a restart. Why it's unavailable is explained in the Pipeline Overview under stage B rather than a sidebar tooltip: the sidebar is too narrow for an inline explanation, and Qt does not reliably show tooltips on disabled QListWidgetItems. The Train Aligners page itself also carries the same note for anyone who reaches it once enabled. --- .../explanatory/pipeline_definitions.yaml | 2 +- src/voxkit/gui/__init__.py | 4 ++ src/voxkit/gui/pages/pipeline/__init__.py | 37 ++++++++++++++++++- .../gui/pages/pipeline/training_stacker.py | 9 +++++ src/voxkit/storage/datasets.py | 10 +++++ 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/config/profiles/explanatory/pipeline_definitions.yaml b/config/profiles/explanatory/pipeline_definitions.yaml index 31c2c0b..9f6b18f 100644 --- a/config/profiles/explanatory/pipeline_definitions.yaml +++ b/config/profiles/explanatory/pipeline_definitions.yaml @@ -38,7 +38,7 @@ pipeline: Training improves alignment accuracy by adapting models to your speakers, recording conditions, or dialect. Requires a dataset that already has alignments (from a pretrained model) to use as training data. - *Skip this step if using pretrained models is sufficient for your needs.* + *Train Aligners is unavailable in the left sidebar until a dataset with manual alignments is registered.* --- diff --git a/src/voxkit/gui/__init__.py b/src/voxkit/gui/__init__.py index 0878851..104fa85 100644 --- a/src/voxkit/gui/__init__.py +++ b/src/voxkit/gui/__init__.py @@ -158,6 +158,10 @@ def build_feedback_mailto_url( QListWidget::item:hover { background-color: #b0cef2; } + QListWidget::item:disabled { + color: #a8a8a8; + background-color: transparent; + } QWidget#centralWidget { background-color: #f5f7fa; } diff --git a/src/voxkit/gui/pages/pipeline/__init__.py b/src/voxkit/gui/pages/pipeline/__init__.py index e74722d..4deb820 100644 --- a/src/voxkit/gui/pages/pipeline/__init__.py +++ b/src/voxkit/gui/pages/pipeline/__init__.py @@ -84,6 +84,7 @@ def on_settings(self): QHBoxLayout, QLabel, QListWidget, + QListWidgetItem, QPushButton, QScrollArea, QVBoxLayout, @@ -92,6 +93,7 @@ def on_settings(self): from voxkit.gui.components import AnimatedStackedWidget from voxkit.gui.styles import Buttons, Containers, Labels +from voxkit.storage import datasets if TYPE_CHECKING: from voxkit.config.pipeline_config import PipelineConfig @@ -164,13 +166,18 @@ def init_ui(self): # Store mapping of menu index to stacker for reload self.stacker_instances = [] + # Store menu items by step id so availability can be toggled later + self._menu_items: dict[str, QListWidgetItem] = {} + # Right side - Stacked widget for different pipeline pages self.stacked_widget = AnimatedStackedWidget() # Dynamically create menu items and stackers from configuration for step in self.config.enabled_steps: # Add menu item - self.menu_list.addItem(step.label) + menu_item = QListWidgetItem(step.label) + self.menu_list.addItem(menu_item) + self._menu_items[step.id] = menu_item # Get the stacker class from registry stacker_class = STACKER_REGISTRY.get(step.stacker_class) @@ -264,6 +271,8 @@ def toggle(): self.menu_list.currentRowChanged.connect(self.change_page) self.menu_list.setCurrentRow(0) + self._refresh_training_availability() + def reload(self): """Reload models and datasets in the pipeline pages. @@ -303,6 +312,32 @@ def reload(self): if hasattr(stacker_widget, "reload_datasets"): stacker_widget.reload_datasets() + self._refresh_training_availability() + + def _refresh_training_availability(self): + """Gray out the Train Aligners step unless a dataset has a manual alignment. + + Training only produces a meaningful model when it can learn from a + hand-corrected alignment, so this step is disabled until at least one + dataset qualifies. Why it's disabled is explained in the Ⓑ Train + Aligners section of the Pipeline Overview, not in the sidebar itself + (too narrow for an inline explanation). + """ + menu_item = self._menu_items.get("training") + stacker_widget = next( + (widget for step_id, _, widget in self.stacker_instances if step_id == "training"), + None, + ) + if menu_item is None or stacker_widget is None: + return + + if datasets.any_dataset_has_manual_alignments(): + menu_item.setFlags(menu_item.flags() | Qt.ItemFlag.ItemIsEnabled) + stacker_widget.setEnabled(True) + else: + menu_item.setFlags(menu_item.flags() & ~Qt.ItemFlag.ItemIsEnabled) + stacker_widget.setEnabled(False) + def change_page(self, index): """Change the displayed page based on menu selection with animation""" if index >= 0: # Valid index diff --git a/src/voxkit/gui/pages/pipeline/training_stacker.py b/src/voxkit/gui/pages/pipeline/training_stacker.py index b11b472..3e238b3 100644 --- a/src/voxkit/gui/pages/pipeline/training_stacker.py +++ b/src/voxkit/gui/pages/pipeline/training_stacker.py @@ -297,6 +297,15 @@ def reload_datasets(self): def build_ui(self): """Build the training UI.""" + note_label = QLabel( + "Note: training only makes sense for datasets with manual (hand-corrected) " + "alignments — models trained on machine-generated alignments alone tend to " + "just reinforce whatever errors are already there." + ) + note_label.setWordWrap(True) + note_label.setStyleSheet(Labels.INFO) + self.content_layout.addWidget(note_label) + # Model Selection Panel engines_dict = { engine_id: engine diff --git a/src/voxkit/storage/datasets.py b/src/voxkit/storage/datasets.py index b67a8fb..cc96f55 100644 --- a/src/voxkit/storage/datasets.py +++ b/src/voxkit/storage/datasets.py @@ -20,6 +20,7 @@ - **create_dataset**: Create a new dataset with metadata and directories - **get_dataset_metadata**: Retrieve metadata for a specific dataset - **list_datasets_metadata**: List all existing datasets +- **any_dataset_has_manual_alignments**: Whether any dataset has a hand alignment - **update_dataset_metadata**: Update metadata fields for a specific dataset - **delete_dataset**: Delete a registered dataset and its metadata - **export_dataset**: Export a dataset to a specified output path @@ -321,6 +322,15 @@ def list_datasets_metadata() -> List[DatasetMetadata]: return [] +def any_dataset_has_manual_alignments() -> bool: + """Whether at least one registered dataset has a hand/manual alignment. + + Training only produces a meaningful model when it can learn from a + manually-corrected alignment, so this gates the Train Aligners step. + """ + return any(d.get("hand_alignments_path") for d in list_datasets_metadata()) + + def update_dataset_metadata( dataset_id: str, updates: dict,