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,