patkit 0.22.2__tar.gz → 0.22.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. {patkit-0.22.2 → patkit-0.22.3}/PKG-INFO +3 -3
  2. {patkit-0.22.2 → patkit-0.22.3}/README.md +1 -1
  3. {patkit-0.22.2 → patkit-0.22.3}/patkit/constants.py +15 -1
  4. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_loader.py +5 -1
  5. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_structures/metadata_classes.py +3 -0
  6. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/__init__.py +3 -1
  7. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/annotator_window.py +11 -0
  8. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/exercise_dialogs.py +120 -2
  9. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/image_save_dialog.py +14 -10
  10. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/plot_controller.py +18 -4
  11. {patkit-0.22.2 → patkit-0.22.3}/patkit/qt_annotator.py +91 -8
  12. {patkit-0.22.2 → patkit-0.22.3}/patkit/save_and_load/__init__.py +3 -0
  13. patkit-0.22.3/patkit/save_and_load/exercise_packaging.py +141 -0
  14. {patkit-0.22.2 → patkit-0.22.3}/patkit/save_and_load/save.py +6 -1
  15. {patkit-0.22.2 → patkit-0.22.3}/pyproject.toml +1 -1
  16. {patkit-0.22.2 → patkit-0.22.3}/.gitignore +0 -0
  17. {patkit-0.22.2 → patkit-0.22.3}/LICENSE.markdown +0 -0
  18. {patkit-0.22.2 → patkit-0.22.3}/patkit/__init__.py +0 -0
  19. {patkit-0.22.2 → patkit-0.22.3}/patkit/annotations/__init__.py +0 -0
  20. {patkit-0.22.2 → patkit-0.22.3}/patkit/annotations/peaks.py +0 -0
  21. {patkit-0.22.2 → patkit-0.22.3}/patkit/audio_processing/__init__.py +0 -0
  22. {patkit-0.22.2 → patkit-0.22.3}/patkit/audio_processing/audio_processing.py +0 -0
  23. {patkit-0.22.2 → patkit-0.22.3}/patkit/cli.py +0 -0
  24. {patkit-0.22.2 → patkit-0.22.3}/patkit/cli_commands.py +0 -0
  25. {patkit-0.22.2 → patkit-0.22.3}/patkit/computational.py +0 -0
  26. {patkit-0.22.2 → patkit-0.22.3}/patkit/configuration/__init__.py +0 -0
  27. {patkit-0.22.2 → patkit-0.22.3}/patkit/configuration/configuration.py +0 -0
  28. {patkit-0.22.2 → patkit-0.22.3}/patkit/configuration/configuration_models.py +0 -0
  29. {patkit-0.22.2 → patkit-0.22.3}/patkit/configuration/configuration_parsers.py +0 -0
  30. {patkit-0.22.2 → patkit-0.22.3}/patkit/configuration/exclusion_list_functions.py +0 -0
  31. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/AAA_raw_ultrasound.py +0 -0
  32. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/AAA_recordings.py +0 -0
  33. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/AAA_splines.py +0 -0
  34. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/RASL_3D_ultrasound_recordings.py +0 -0
  35. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/__init__.py +0 -0
  36. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/audio.py +0 -0
  37. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/session_import_config.py +0 -0
  38. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/spline_import_config.py +0 -0
  39. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/three_dim_ultrasound.py +0 -0
  40. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/video.py +0 -0
  41. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_import/wav_recordings.py +0 -0
  42. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_processor.py +0 -0
  43. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_structures/__init__.py +0 -0
  44. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_structures/base_classes.py +0 -0
  45. {patkit-0.22.2 → patkit-0.22.3}/patkit/data_structures/data_structures.py +0 -0
  46. {patkit-0.22.2 → patkit-0.22.3}/patkit/default_configuration/patkit-data.yaml +0 -0
  47. {patkit-0.22.2 → patkit-0.22.3}/patkit/default_configuration/patkit-gui.yaml +0 -0
  48. {patkit-0.22.2 → patkit-0.22.3}/patkit/default_configuration/patkit-publish.yaml +0 -0
  49. {patkit-0.22.2 → patkit-0.22.3}/patkit/default_configuration/patkit_logging_configuration.json +0 -0
  50. {patkit-0.22.2 → patkit-0.22.3}/patkit/errors.py +0 -0
  51. {patkit-0.22.2 → patkit-0.22.3}/patkit/export/__init__.py +0 -0
  52. {patkit-0.22.2 → patkit-0.22.3}/patkit/export/images.py +0 -0
  53. {patkit-0.22.2 → patkit-0.22.3}/patkit/export/metadata.py +0 -0
  54. {patkit-0.22.2 → patkit-0.22.3}/patkit/export/modality_data.py +0 -0
  55. {patkit-0.22.2 → patkit-0.22.3}/patkit/external_class_extensions/__init__.py +0 -0
  56. {patkit-0.22.2 → patkit-0.22.3}/patkit/external_class_extensions/base_model_extensions.py +0 -0
  57. {patkit-0.22.2 → patkit-0.22.3}/patkit/external_class_extensions/enum_extensions.py +0 -0
  58. {patkit-0.22.2 → patkit-0.22.3}/patkit/get_modality_types.py +0 -0
  59. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/audio_player.py +0 -0
  60. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/boundary_animation.py +0 -0
  61. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/dialog_template.py +0 -0
  62. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/list_save_dialog.py +0 -0
  63. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/list_selection_dialog.py +0 -0
  64. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/ofbrowse.py +0 -0
  65. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/player_controls.py +0 -0
  66. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/replace_dialog.py +0 -0
  67. {patkit-0.22.2 → patkit-0.22.3}/patkit/gui/select_scenario_dialog.py +0 -0
  68. {patkit-0.22.2 → patkit-0.22.3}/patkit/import_formats/__init__.py +0 -0
  69. {patkit-0.22.2 → patkit-0.22.3}/patkit/import_formats/avi_read.py +0 -0
  70. {patkit-0.22.2 → patkit-0.22.3}/patkit/import_formats/dicom_ultra_read.py +0 -0
  71. {patkit-0.22.2 → patkit-0.22.3}/patkit/import_formats/rasl_dat_to_wav.py +0 -0
  72. {patkit-0.22.2 → patkit-0.22.3}/patkit/import_formats/splines_read.py +0 -0
  73. {patkit-0.22.2 → patkit-0.22.3}/patkit/import_formats/ult_read.py +0 -0
  74. {patkit-0.22.2 → patkit-0.22.3}/patkit/import_formats/wav_read.py +0 -0
  75. {patkit-0.22.2 → patkit-0.22.3}/patkit/initialise.py +0 -0
  76. {patkit-0.22.2 → patkit-0.22.3}/patkit/interpolate_raw_uti.py +0 -0
  77. {patkit-0.22.2 → patkit-0.22.3}/patkit/interpreter.py +0 -0
  78. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/__init__.py +0 -0
  79. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/aggregate_image.py +0 -0
  80. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/calculate_aggregate_images.py +0 -0
  81. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/calculate_distance_matrices.py +0 -0
  82. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/calculate_intensity.py +0 -0
  83. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/calculate_pd.py +0 -0
  84. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/calculate_spline_metric.py +0 -0
  85. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/distance_matrix.py +0 -0
  86. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/downsample_metric.py +0 -0
  87. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/intensity.py +0 -0
  88. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/metrics_helpers.py +0 -0
  89. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/ofreg.py +0 -0
  90. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/pd.py +0 -0
  91. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/spline_metric.py +0 -0
  92. {patkit-0.22.2 → patkit-0.22.3}/patkit/metrics/tongue_shape_analysis.py +0 -0
  93. {patkit-0.22.2 → patkit-0.22.3}/patkit/modalities/__init__.py +0 -0
  94. {patkit-0.22.2 → patkit-0.22.3}/patkit/modalities/recorded_metadata_classes.py +0 -0
  95. {patkit-0.22.2 → patkit-0.22.3}/patkit/modalities/recorded_modalities.py +0 -0
  96. {patkit-0.22.2 → patkit-0.22.3}/patkit/modalities/splines.py +0 -0
  97. {patkit-0.22.2 → patkit-0.22.3}/patkit/patgrid.py +0 -0
  98. {patkit-0.22.2 → patkit-0.22.3}/patkit/path_resolution.py +0 -0
  99. {patkit-0.22.2 → patkit-0.22.3}/patkit/plot_and_publish/__init__.py +0 -0
  100. {patkit-0.22.2 → patkit-0.22.3}/patkit/plot_and_publish/legend_generation.py +0 -0
  101. {patkit-0.22.2 → patkit-0.22.3}/patkit/plot_and_publish/plot.py +0 -0
  102. {patkit-0.22.2 → patkit-0.22.3}/patkit/plot_and_publish/plot_utilities.py +0 -0
  103. {patkit-0.22.2 → patkit-0.22.3}/patkit/plot_and_publish/publish.py +0 -0
  104. {patkit-0.22.2 → patkit-0.22.3}/patkit/save_and_load/load.py +0 -0
  105. {patkit-0.22.2 → patkit-0.22.3}/patkit/save_and_load/save_and_load_schemas.py +0 -0
  106. {patkit-0.22.2 → patkit-0.22.3}/patkit/simulation/__init__.py +0 -0
  107. {patkit-0.22.2 → patkit-0.22.3}/patkit/simulation/contour_tools.py +0 -0
  108. {patkit-0.22.2 → patkit-0.22.3}/patkit/simulation/metric_calculations.py +0 -0
  109. {patkit-0.22.2 → patkit-0.22.3}/patkit/simulation/perturbation_series_plots.py +0 -0
  110. {patkit-0.22.2 → patkit-0.22.3}/patkit/simulation/rays_on_contours.py +0 -0
  111. {patkit-0.22.2 → patkit-0.22.3}/patkit/simulation/simulate.py +0 -0
  112. {patkit-0.22.2 → patkit-0.22.3}/patkit/simulation/simulation_datastructures.py +0 -0
  113. {patkit-0.22.2 → patkit-0.22.3}/patkit/simulation/simulation_plots.py +0 -0
  114. {patkit-0.22.2 → patkit-0.22.3}/patkit/ui_callbacks.py +0 -0
  115. {patkit-0.22.2 → patkit-0.22.3}/patkit/utility_functions/__init__.py +0 -0
  116. {patkit-0.22.2 → patkit-0.22.3}/patkit/utility_functions/logging_helpers.py +0 -0
  117. {patkit-0.22.2 → patkit-0.22.3}/patkit/utility_functions/path.py +0 -0
  118. {patkit-0.22.2 → patkit-0.22.3}/patkit/utility_functions/processing_helpers.py +0 -0
  119. {patkit-0.22.2 → patkit-0.22.3}/patkit/utility_functions/string_formatters.py +0 -0
  120. {patkit-0.22.2 → patkit-0.22.3}/patkit/utility_functions/types.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: patkit
3
- Version: 0.22.2
3
+ Version: 0.22.3
4
4
  Summary: Phonetic Analysis ToolKit: Tools for processing phonetic data
5
5
  Project-URL: bug_tracker, https://github.com/giuthas/patkit/issues
6
6
  Project-URL: changelog, https://github.com/giuthas/patkit/blob/main/docs/Release_notes.markdown
@@ -718,7 +718,7 @@ Description-Content-Type: text/markdown
718
718
  [![test](https://github.com/giuthas/patkit/actions/workflows/test.yaml/badge.svg)](https://github.com/giuthas/patkit/actions/workflows/test.yaml)
719
719
  # PATKIT - Phonetic Analysis ToolKIT
720
720
 
721
- ![PATKIT GUI](https://giuthas.github.io/patkit/PATKIT_UI_0.22.0.png)
721
+ ![PATKIT GUI](https://giuthas.github.io/patkit/PATKIT_UI_0.22.2.png)
722
722
 
723
723
  PATKIT provides tools for phonetic analysis of speech data. It includes a GUI
724
724
  for manual assessment/analysis/annotation (see picture above), command line
@@ -1,7 +1,7 @@
1
1
  [![test](https://github.com/giuthas/patkit/actions/workflows/test.yaml/badge.svg)](https://github.com/giuthas/patkit/actions/workflows/test.yaml)
2
2
  # PATKIT - Phonetic Analysis ToolKIT
3
3
 
4
- ![PATKIT GUI](https://giuthas.github.io/patkit/PATKIT_UI_0.22.0.png)
4
+ ![PATKIT GUI](https://giuthas.github.io/patkit/PATKIT_UI_0.22.2.png)
5
5
 
6
6
  PATKIT provides tools for phonetic analysis of speech data. It includes a GUI
7
7
  for manual assessment/analysis/annotation (see picture above), command line
@@ -510,7 +510,21 @@ class DefaultCursorColors(str, Enum):
510
510
  Default colors for GUI cursor and selection elements.
511
511
  """
512
512
  SELECTION = "deepskyblue"
513
- PLAYBACK = "red"
513
+ PLAYBACK = "#FD7400"
514
+
515
+ def __str__(self) -> str:
516
+ """
517
+ Ensure f-strings and print() output the string value.
518
+ """
519
+ return self.value
520
+
521
+
522
+ class ColorPalettes(str, Enum):
523
+ """
524
+ Colors for plotting lines.
525
+ """
526
+ COLORBLIND_10 = 'tableau-colorblind10'
527
+ # COLORBLIND_6 = 'viscid-colorblind'
514
528
 
515
529
  def __str__(self) -> str:
516
530
  """
@@ -148,9 +148,13 @@ def read_recorded_session_from_dir(
148
148
  )
149
149
  return session
150
150
 
151
+ # TODO 1.0: if Sessions are no longer the highest possible container tier,
152
+ # this logic needs to change.
151
153
  file_info = FileInformation(
152
154
  recorded_path=recorded_data_path,
153
- recorded_meta_file=session_config_path.name)
155
+ recorded_meta_file=session_config_path.name,
156
+ is_top_level_object=True,
157
+ )
154
158
  if session_config_path.is_file():
155
159
  session_config = load_session_config(
156
160
  recorded_data_path, session_config_path)
@@ -69,6 +69,8 @@ class FileInformation:
69
69
  Name of the patkit meta file, if it exists. Defaults to None.
70
70
  patkit_path : Path | None
71
71
  Path to the saved patkit data, if it exists. Defaults to None.
72
+ is_top_level_object : bool
73
+ Is this the top level container, by default False.
72
74
  """
73
75
  recorded_data_file: str | None = None
74
76
  recorded_meta_file: str | None = None
@@ -76,6 +78,7 @@ class FileInformation:
76
78
  patkit_data_file: str | None = None
77
79
  patkit_meta_file: str | None = None
78
80
  patkit_path: Path | None = None
81
+ is_top_level_object: bool = False
79
82
 
80
83
  @property
81
84
  def basename(self) -> str:
@@ -34,7 +34,9 @@
34
34
  from .annotator_window import UiMainWindow
35
35
  from .audio_player import AudioPlayer
36
36
  from .boundary_animation import BoundaryAnimator
37
- from .exercise_dialogs import NewAnswerDialog, NewExerciseDialog
37
+ from .exercise_dialogs import (
38
+ PackageExerciseDialog, NewAnswerDialog, NewExerciseDialog
39
+ )
38
40
  from .image_save_dialog import ImageSaveDialog
39
41
  from .list_save_dialog import ListSaveDialog
40
42
  from .list_selection_dialog import ListSelectionDialog
@@ -275,6 +275,11 @@ class UiMainWindow(object):
275
275
  self.action_new_exercise.setObjectName("action_new_exercise")
276
276
  self.action_save_exercise = QtGui.QAction(main_window)
277
277
  self.action_save_exercise.setObjectName("action_save_exercise")
278
+ self.action_package_exercise = QtGui.QAction(main_window)
279
+ self.action_package_exercise.setObjectName("action_package_exercise")
280
+ self.action_unpackage_exercise = QtGui.QAction(main_window)
281
+ self.action_unpackage_exercise.setObjectName(
282
+ "action_unpackage_exercise")
278
283
  self.action_new_answer = QtGui.QAction(main_window)
279
284
  self.action_new_answer.setObjectName("action_new_answer")
280
285
  self.action_save_answer = QtGui.QAction(main_window)
@@ -289,6 +294,8 @@ class UiMainWindow(object):
289
294
 
290
295
  self.menu_exercise.addAction(self.action_new_exercise)
291
296
  self.menu_exercise.addAction(self.action_save_exercise)
297
+ self.menu_exercise.addAction(self.action_package_exercise)
298
+ self.menu_exercise.addAction(self.action_unpackage_exercise)
292
299
  self.menu_exercise.addSeparator()
293
300
  self.menu_exercise.addAction(self.action_new_answer)
294
301
  self.menu_exercise.addAction(self.action_save_answer)
@@ -450,6 +457,10 @@ class UiMainWindow(object):
450
457
  _translate("MainWindow", "Ctrl+Shift+N"))
451
458
  self.action_save_exercise.setText(
452
459
  _translate("MainWindow", "Save exercise"))
460
+ self.action_package_exercise.setText(
461
+ _translate("MainWindow", "Package exercise..."))
462
+ self.action_unpackage_exercise.setText(
463
+ _translate("MainWindow", "Unpackage exercise..."))
453
464
  self.action_new_answer.setText(
454
465
  _translate("MainWindow", "New answer..."))
455
466
  self.action_new_answer.setShortcut(
@@ -36,8 +36,8 @@ Dialogs for Exercises and Answers.
36
36
  from pathlib import Path
37
37
 
38
38
  from PyQt6.QtWidgets import (
39
- QComboBox, QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout,
40
- QLabel, QLineEdit, QSizePolicy, QVBoxLayout, QWidget
39
+ QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout,
40
+ QLabel, QLineEdit, QPushButton, QSizePolicy, QVBoxLayout, QWidget
41
41
  )
42
42
 
43
43
  from patkit.constants import ExerciseScrambler
@@ -188,3 +188,121 @@ class NewAnswerDialog(QDialog):
188
188
  if dialog.exec() == QDialog.DialogCode.Rejected:
189
189
  return None, None
190
190
  return dialog.answer_name, dialog.author_name
191
+
192
+
193
+ class PackageExerciseDialog(QDialog):
194
+ """
195
+ Dialog for configuring an Exercise packaging.
196
+
197
+ This dialog allows the user to specify a package path and choose whether
198
+ to include the unscrambled Session TextGrids in the resulting archive.
199
+ """
200
+
201
+ def __init__(
202
+ self,
203
+ parent: QWidget | None = None,
204
+ default_path: Path | None = None
205
+ ) -> None:
206
+ """
207
+ Initialize the PackageExerciseDialog.
208
+
209
+ Parameters
210
+ ----------
211
+ parent : QWidget | None, optional
212
+ The parent widget, by default None.
213
+ default_path : Path | None, optional
214
+ The default file path to populate the dialog, by default None.
215
+ """
216
+ super().__init__(parent=parent)
217
+ self.setWindowTitle("Package Exercise")
218
+ self.resize(700, 100)
219
+
220
+ self.package_path: Path | None = None
221
+ self.include_textgrids: bool = False
222
+
223
+ self.path_label = QLabel(text="Package Path:", parent=self)
224
+ self.path_field = QLineEdit(parent=self)
225
+ if default_path is not None:
226
+ self.path_field.setText(str(default_path))
227
+
228
+ self.browse_button = QPushButton(text="Browse...", parent=self)
229
+ self.browse_button.clicked.connect(slot=self._browse)
230
+
231
+ path_layout = QHBoxLayout()
232
+ path_layout.addWidget(self.path_label)
233
+ path_layout.addWidget(self.path_field)
234
+ path_layout.addWidget(self.browse_button)
235
+
236
+ self.textgrid_checkbox = QCheckBox(
237
+ text=(
238
+ "Include original TextGrids (this will make "
239
+ "them viewable when running the exercise)."
240
+ ),
241
+ parent=self
242
+ )
243
+
244
+ dialog_buttons = (
245
+ QDialogButtonBox.StandardButton.Ok |
246
+ QDialogButtonBox.StandardButton.Cancel
247
+ )
248
+ self.ok_cancel_buttons = QDialogButtonBox(dialog_buttons)
249
+ self.ok_cancel_buttons.accepted.connect(slot=self._on_accepted)
250
+ self.ok_cancel_buttons.rejected.connect(slot=self.reject)
251
+
252
+ main_layout = QVBoxLayout(self)
253
+ main_layout.addLayout(path_layout)
254
+ main_layout.addWidget(self.textgrid_checkbox)
255
+ main_layout.addWidget(self.ok_cancel_buttons)
256
+
257
+ self.setMinimumWidth(450)
258
+
259
+ def _browse(self) -> None:
260
+ """Open a file dialog to select the destination zip path."""
261
+ file_path, _ = QFileDialog.getSaveFileName(
262
+ parent=self,
263
+ caption="Select Packaging Destination",
264
+ directory=self.path_field.text(),
265
+ filter="Zip Files (*.zip)"
266
+ )
267
+ if file_path:
268
+ self.path_field.setText(file_path)
269
+
270
+ def _on_accepted(self) -> None:
271
+ """Validate input and accept the dialog."""
272
+ path_text = self.path_field.text()
273
+ if not path_text:
274
+ self.reject()
275
+ return
276
+
277
+ self.package_path = Path(path_text)
278
+ self.include_textgrids = self.textgrid_checkbox.isChecked()
279
+ self.accept()
280
+
281
+ @staticmethod
282
+ def get_packaging_params(
283
+ parent: QWidget | None = None,
284
+ default_path: Path | None = None
285
+ ) -> tuple[Path | None, bool]:
286
+ """
287
+ Open the package dialog and query the user for packaging parameters.
288
+
289
+ Parameters
290
+ ----------
291
+ parent : QWidget | None, optional
292
+ Parent window.
293
+ default_path : Path | None, optional
294
+ The default file path to populate the line edit.
295
+
296
+ Returns
297
+ -------
298
+ tuple[Path | None, bool]
299
+ The selected file path and boolean indicating if TextGrids
300
+ should be included. Path is None if the user cancelled.
301
+ """
302
+ dialog = PackageExerciseDialog(
303
+ parent=parent, default_path=default_path
304
+ )
305
+ if dialog.exec() == QDialog.DialogCode.Rejected:
306
+ return None, False
307
+
308
+ return dialog.package_path, dialog.include_textgrids
@@ -34,12 +34,14 @@ import os
34
34
  from pathlib import Path
35
35
 
36
36
  from PyQt6 import QtCore
37
- from PyQt6.QtGui import QFontMetrics, QIcon
37
+ from PyQt6.QtGui import QIcon
38
38
  from PyQt6.QtWidgets import (
39
39
  QCheckBox, QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, QLabel,
40
40
  QLineEdit, QPushButton, QVBoxLayout, QWidget
41
41
  )
42
42
 
43
+ # TODO 1.0: docstrings
44
+
43
45
 
44
46
  class ImageSaveDialog(QDialog):
45
47
 
@@ -68,14 +70,14 @@ class ImageSaveDialog(QDialog):
68
70
  option_box = None
69
71
  if options is not None:
70
72
  option_box = QHBoxLayout()
71
- self.option_checkboxes = {}
73
+ self.checkboxes = {}
72
74
  for option, checked in options.items():
73
75
  checkbox = QCheckBox(option)
74
76
  check = (QtCore.Qt.CheckState.Checked
75
77
  if checked else QtCore.Qt.CheckState.Unchecked)
76
78
  checkbox.setCheckState(check)
77
- self.option_checkboxes[option] = checkbox
78
- option_box.addWidget(self.option_checkboxes[option])
79
+ self.checkboxes[option] = checkbox
80
+ option_box.addWidget(self.checkboxes[option])
79
81
  path_and_name_box = QHBoxLayout()
80
82
  self.path_label = QLabel(self)
81
83
  self.path_label.setText("Path:")
@@ -110,7 +112,7 @@ class ImageSaveDialog(QDialog):
110
112
  vbox.addLayout(option_box)
111
113
  vbox.addLayout(path_and_name_box)
112
114
  vbox.addWidget(self.ok_cancel_buttons)
113
-
115
+
114
116
  self.setWindowTitle(self.name)
115
117
  if icon:
116
118
  self.setWindowIcon(icon)
@@ -118,19 +120,21 @@ class ImageSaveDialog(QDialog):
118
120
  self.adjustSize()
119
121
 
120
122
  def _browse(self):
121
- directory = QFileDialog.getSaveFileName(
123
+ # TODO 0.23: getSaveFileName doesn't necessarily match the purpose
124
+ # here?
125
+ path, something = QFileDialog.getSaveFileName(
122
126
  parent=self,
123
- caption="Select Directory to Export to",
127
+ caption="Select Path to Export to",
124
128
  directory=self.path_field.text(),
125
129
  options=QFileDialog.Option.DontResolveSymlinks
126
130
  )
127
- if directory:
128
- self.path_field.setText(directory)
131
+ if path:
132
+ self.path_field.setText(path)
129
133
 
130
134
  def _on_accepted(self):
131
135
  if self.options is not None:
132
136
  for option in self.options:
133
- self.options[option] = self.option_checkboxes[option].isChecked()
137
+ self.options[option] = self.checkboxes[option].isChecked()
134
138
  self.save_path = Path(self.path_field.text())
135
139
  self.accept()
136
140
 
@@ -44,7 +44,7 @@ import numpy as np
44
44
  from patkit.data_structures import Recording
45
45
  from patkit.configuration import DataConfig, GuiConfig
46
46
  from patkit.constants import (
47
- AnnotatorMode, DefaultCanvasColors, DefaultCursorColors,
47
+ AnnotatorMode, ColorPalettes, DefaultCanvasColors, DefaultCursorColors,
48
48
  ExerciseMode, GuiColorScheme, GuiImageType
49
49
  )
50
50
  from patkit.plot_and_publish import (
@@ -136,7 +136,7 @@ class PlotController:
136
136
  matplotlib.rcParams.update(
137
137
  {'font.size': self.gui_config.default_font_size}
138
138
  )
139
- plt.style.use('tableau-colorblind10')
139
+ plt.style.use(ColorPalettes.COLORBLIND_10)
140
140
 
141
141
  def to_annotator_mode(
142
142
  self,
@@ -156,6 +156,7 @@ class PlotController:
156
156
  raise ValueError(
157
157
  f"Unknown GUI color scheme: {gui_color_mode}"
158
158
  )
159
+ plt.style.use(ColorPalettes.COLORBLIND_10)
159
160
 
160
161
  def to_exercise_mode(
161
162
  self,
@@ -175,6 +176,7 @@ class PlotController:
175
176
  raise ValueError(
176
177
  f"Unknown GUI color scheme: {gui_color_mode}"
177
178
  )
179
+ plt.style.use(ColorPalettes.COLORBLIND_10)
178
180
 
179
181
  def to_example_mode(
180
182
  self,
@@ -194,6 +196,7 @@ class PlotController:
194
196
  raise ValueError(
195
197
  f"Unknown GUI color scheme: {gui_color_mode}"
196
198
  )
199
+ plt.style.use(ColorPalettes.COLORBLIND_10)
197
200
 
198
201
  def to_answer_mode(
199
202
  self,
@@ -213,6 +216,7 @@ class PlotController:
213
216
  raise ValueError(
214
217
  f"Unknown GUI color scheme: {gui_color_mode}"
215
218
  )
219
+ plt.style.use(ColorPalettes.COLORBLIND_10)
216
220
 
217
221
  def setup_axes(self) -> None:
218
222
  """
@@ -416,8 +420,11 @@ class PlotController:
416
420
  )
417
421
  boundaries_by_axis.append(boundary_set)
418
422
  axis.set_ylabel(
419
- name, rotation=90, horizontalalignment="center",
420
- verticalalignment="center")
423
+ name,
424
+ rotation=0,
425
+ horizontalalignment="center",
426
+ verticalalignment="center"
427
+ )
421
428
  axis.set_xlim(xlim)
422
429
  if name in self.gui_config.pervasive_tiers:
423
430
  for data_axis in self.data_axes:
@@ -903,3 +910,10 @@ class PlotController:
903
910
  self.canvas.draw()
904
911
 
905
912
  self.update_multicursor()
913
+
914
+ def display_exclusion(self):
915
+ """
916
+ Updates title and graphs to show this Recording is excluded.
917
+ """
918
+ # TODO 0.23: do this correctly
919
+ pass
@@ -76,14 +76,14 @@ from patkit.export import (
76
76
  from patkit.gui import (
77
77
  AudioPlayer, ImageSaveDialog, ListSaveDialog,
78
78
  NewAnswerDialog, NewExerciseDialog,
79
- ListSelectionDialog, PlotController,
79
+ ListSelectionDialog, PackageExerciseDialog, PlotController,
80
80
  ReplaceDialog, UiMainWindow,
81
81
  )
82
82
  from patkit.initialise import initialise_config, initialise_patkit
83
83
  from patkit.path_resolution import get_manifest_scenarios, resolve_open_path
84
84
  from patkit.save_and_load import (
85
- load_answer, save_answer, save_exercise,
86
- save_recording_session,
85
+ load_answer, package_exercise_to_zip, unpackage_exercise_from_zip,
86
+ save_answer, save_exercise, save_recording_session,
87
87
  )
88
88
  from patkit.ui_callbacks import UiCallbacks
89
89
 
@@ -203,6 +203,9 @@ class PdQtAnnotator(QMainWindow, UiMainWindow):
203
203
  self.action_new_exercise.triggered.connect(
204
204
  self.new_exercise)
205
205
  self.action_save_exercise.triggered.connect(self.save_exercise)
206
+ self.action_package_exercise.triggered.connect(self.package_exercise)
207
+ self.action_unpackage_exercise.triggered.connect(
208
+ self.unpackage_exercise)
206
209
  self.action_new_answer.triggered.connect(self.new_answer)
207
210
  self.action_save_answer.triggered.connect(self.save_answer)
208
211
  self.action_open_answer.triggered.connect(self.open_answer)
@@ -709,7 +712,7 @@ class PdQtAnnotator(QMainWindow, UiMainWindow):
709
712
 
710
713
  self.update()
711
714
  # In case labels change as new plots are drawn.
712
- self.figure.align_ylabels()
715
+ self.plot_controller.figure.align_ylabels()
713
716
  self.update_ui()
714
717
 
715
718
  def open_file(self):
@@ -727,7 +730,7 @@ class PdQtAnnotator(QMainWindow, UiMainWindow):
727
730
  """
728
731
  Save derived modalities and annotations.
729
732
  """
730
- # TODO 0.22.3: does this save textgrids too and how does it interact
733
+ # TODO 0.23.3: does this save textgrids too and how does it interact
731
734
  # with saving answers and exercises.
732
735
  save_recording_session(self.session)
733
736
 
@@ -755,7 +758,7 @@ class PdQtAnnotator(QMainWindow, UiMainWindow):
755
758
  """
756
759
  Save the all TextGrids in this Session.
757
760
  """
758
- # TODO 0.22.3: write a call back for asking for overwrite confirmation.
761
+ # TODO 0.23.3: write a call back for asking for overwrite confirmation.
759
762
  if self.annotator_mode is AnnotatorMode.EXERCISE:
760
763
  return
761
764
 
@@ -809,6 +812,85 @@ class PdQtAnnotator(QMainWindow, UiMainWindow):
809
812
  """Save the active exercise to disk."""
810
813
  save_exercise(exercise=self.session.exercise)
811
814
 
815
+ def package_exercise(self) -> None:
816
+ """
817
+ Package the active exercise to a zip.
818
+ """
819
+ default_name = f"{self.session.patkit_path.name}.zip"
820
+ default_path = self.session.patkit_path.parent / default_name
821
+
822
+ zip_path, include_grids = PackageExerciseDialog.get_packaging_params(
823
+ parent=self,
824
+ default_path=default_path
825
+ )
826
+
827
+ if zip_path is None:
828
+ return
829
+
830
+ current_answer_name = self.session.exercise.current_answer.name
831
+
832
+ package_exercise_to_zip(
833
+ file_info=self.session.file_info,
834
+ zip_path=zip_path,
835
+ active_answer_name=current_answer_name,
836
+ include_root_textgrids=include_grids
837
+ )
838
+
839
+ # TODO: status message instead
840
+ # QMessageBox.information(
841
+ # parent=self,
842
+ # title="Export Successful",
843
+ # text=f"Exercise successfully exported to:\n{zip_path}"
844
+ # )
845
+
846
+ def unpackage_exercise(self) -> None:
847
+ """
848
+ Prompt the user to extract a zipped exercise and load it.
849
+ """
850
+ zip_path_str, _ = QFileDialog.getOpenFileName(
851
+ parent=self,
852
+ caption="Open Zipped Exercise",
853
+ directory=str(Path.cwd()),
854
+ filter="Zip Files (*.zip)"
855
+ )
856
+
857
+ if not zip_path_str:
858
+ return
859
+
860
+ zip_path = Path(zip_path_str)
861
+
862
+ dest_dir_str = QFileDialog.getExistingDirectory(
863
+ parent=self,
864
+ caption="Select Destination for Extraction",
865
+ directory=str(zip_path.parent)
866
+ )
867
+
868
+ if not dest_dir_str:
869
+ return
870
+
871
+ dest_dir = Path(dest_dir_str) / zip_path.stem
872
+
873
+ # TODO 0.25: ask for confirmation before overwriting instead of just
874
+ # refusing. This should take the form of a callback handed to
875
+ # unpackage_exercise_from_zip, so that individual files can be
876
+ # confirmed as needed.
877
+ unpackage_exercise_from_zip(
878
+ zip_filepath=zip_path,
879
+ destination_directory=dest_dir
880
+ )
881
+
882
+ # TODO: status message instead
883
+ # msg_text = (
884
+ # f"Exercise extracted to:\n{dest_dir}\n\n"
885
+ # "Please open this directory as a new Session."
886
+ # )
887
+
888
+ # QMessageBox.information(
889
+ # parent=self,
890
+ # title="Import Successful",
891
+ # text=msg_text
892
+ # )
893
+
812
894
  def new_answer(self) -> bool:
813
895
  """Create a new blank answer for the current exercise."""
814
896
  answer_name, author_name = NewAnswerDialog.get_answer_params(self)
@@ -854,7 +936,7 @@ class PdQtAnnotator(QMainWindow, UiMainWindow):
854
936
  if not ok or not answer_name:
855
937
  return
856
938
 
857
- # TODO 0.22.3: The loading here might be redundant, if Exercise already
939
+ # TODO 0.23.3: The loading here might be redundant, if Exercise already
858
940
  # front loads every answer.
859
941
  directory = answers_dir / answer_name
860
942
  answer = load_answer(
@@ -990,7 +1072,8 @@ class PdQtAnnotator(QMainWindow, UiMainWindow):
990
1072
  parent=self,
991
1073
  )
992
1074
  if filename is not None:
993
- self.figure.savefig(filename, bbox_inches='tight', pad_inches=0.05)
1075
+ self.plot_controller.figure.savefig(
1076
+ filename, bbox_inches='tight', pad_inches=0.05)
994
1077
  export_session_and_recording_meta(
995
1078
  filename=filename,
996
1079
  session=self.session,
@@ -29,6 +29,9 @@
29
29
  # articles listed in README.md. They can also be found in
30
30
  # citations.bib in BibTeX format.
31
31
  #
32
+ from .exercise_packaging import (
33
+ package_exercise_to_zip, unpackage_exercise_from_zip
34
+ )
32
35
  from .load import (
33
36
  load_answer, load_exercise, load_recordings, load_recording_session
34
37
  )
@@ -0,0 +1,141 @@
1
+ #
2
+ # Copyright (c) 2019-2026
3
+ # Pertti Palo, Scott Moisik, Matthew Faytak, and Motoki Saito.
4
+ #
5
+ # This file is part of the Phonetic Analysis ToolKIT
6
+ # (see https://github.com/giuthas/patkit/).
7
+ #
8
+ # This program is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
+ #
21
+ # The example data packaged with this program is licensed under the
22
+ # Creative Commons Attribution-NonCommercial-ShareAlike 4.0
23
+ # International (CC BY-NC-SA 4.0) License. You should have received a
24
+ # copy of the Creative Commons Attribution-NonCommercial-ShareAlike 4.0
25
+ # International (CC BY-NC-SA 4.0) License along with the data. If not,
26
+ # see <https://creativecommons.org/licenses/by-nc-sa/4.0/> for details.
27
+ #
28
+ # When using the toolkit for scientific publications, please cite the
29
+ # articles listed in README.md. They can also be found in
30
+ # citations.bib in BibTeX format.
31
+ #
32
+ """
33
+ Utility functions for packaging and importing Patkit Exercises.
34
+ """
35
+
36
+ import logging
37
+ from zipfile import ZipFile, ZIP_DEFLATED
38
+ from pathlib import Path
39
+
40
+ from patkit.constants import SourceSuffix
41
+ from patkit.data_structures import FileInformation
42
+
43
+ _logger = logging.getLogger('patkit.exercise_packaging')
44
+
45
+
46
+ def package_exercise_to_zip(
47
+ file_info: FileInformation,
48
+ zip_path: Path,
49
+ active_answer_name: str,
50
+ include_root_textgrids: bool = False,
51
+ include_wav_files: bool = True,
52
+ ) -> None:
53
+ """
54
+ Package a Patkit Session and Exercise into a zip file.
55
+
56
+ Please note that, if the wav files will be included in the zip, they will
57
+ be included in the *patkit directory* rather than as a separate recorded
58
+ data directory. This is regardless of where they are originally situated
59
+ and done to keep the whole exercise contained in one directory.
60
+
61
+ Parameters
62
+ ----------
63
+ file_info : FileInformation
64
+ The FileInformation for the Session.
65
+ zip_filepath : Path
66
+ The destination path for the output `.zip` file.
67
+ active_answer_name : str
68
+ The name of the specific answer to include in the archive. It will be
69
+ renamed to 'answer' in the resulting zip structure.
70
+ include_root_textgrids : bool, optional
71
+ Whether to include root-level `.TextGrid` files, by default False.
72
+ include_wav_files : bool, optional
73
+ Whether to include wav files, by default True.
74
+ """
75
+ _logger.info('Packing an exercise.')
76
+ with ZipFile(file=zip_path, mode='w', compression=ZIP_DEFLATED) as output:
77
+ patkit_path = file_info.patkit_path
78
+ for item in patkit_path.rglob('*'):
79
+ # Only include files.
80
+ if not item.is_file():
81
+ continue
82
+
83
+ # Skip root-level TextGrids if they are not being included.
84
+ if (
85
+ not include_root_textgrids and
86
+ item.parent == file_info.patkit_path and
87
+ (item.suffix.lower() == SourceSuffix.TEXTGRID.lower())
88
+ ):
89
+ _logger.info('skipping: %s', str(item))
90
+ continue
91
+ else:
92
+ _logger.info('including: %s', str(item))
93
+
94
+ relative_path = item.relative_to(patkit_path)
95
+
96
+ # Process the exercise directory to filter answers
97
+ if relative_path.parts[0] == 'exercise':
98
+ if (
99
+ len(relative_path.parts) >= 3 and
100
+ relative_path.parts[1] == 'answers'
101
+ ):
102
+ # Only include the targeted answer,
103
+ # and rename it to 'answer'
104
+ if relative_path.parts[2] == active_answer_name:
105
+ new_parts = list(relative_path.parts)
106
+ new_parts[2] = 'answer'
107
+ arcname = Path(*new_parts)
108
+ output.write(filename=item, arcname=arcname)
109
+ continue # Skip all other answer directories
110
+
111
+ # Write all other allowed files
112
+ output.write(filename=item, arcname=relative_path)
113
+
114
+ recorded_path = file_info.recorded_path
115
+ if recorded_path is not None:
116
+ for item in recorded_path.glob('*' + SourceSuffix.WAV):
117
+ if not item.is_file():
118
+ continue
119
+
120
+ output.write(filename=item, arcname=item.name)
121
+
122
+ # TODO 0.24: verify that wavs or in future other data files are
123
+ # included
124
+
125
+
126
+ def unpackage_exercise_from_zip(
127
+ zip_filepath: Path,
128
+ destination_directory: Path
129
+ ) -> None:
130
+ """
131
+ Extract a Patkit Exercise zip archive.
132
+
133
+ Parameters
134
+ ----------
135
+ zip_filepath : Path
136
+ The path to the `.zip` file to extract.
137
+ destination_directory : Path
138
+ The folder where the archive's contents should be unpacked.
139
+ """
140
+ with ZipFile(file=zip_filepath, mode='r') as zip_file:
141
+ zip_file.extractall(path=destination_directory)
@@ -387,7 +387,12 @@ def save_session_meta(
387
387
  meta['format_version'] = PATKIT_FILE_VERSION
388
388
 
389
389
  parameters = OrderedDict()
390
- parameters['patkit_path'] = str(session.patkit_path.resolve())
390
+ if session.file_info.is_top_level_object:
391
+ parameters['patkit_path'] = '.'
392
+ else:
393
+ # TODO 0.23.3: The next two should probably not be resolved, but this
394
+ # may break something else, so test.
395
+ parameters['patkit_path'] = str(session.patkit_path.resolve())
391
396
  parameters['recorded_path'] = str(session.recorded_path.resolve())
392
397
  parameters['datasource_name'] = session.metadata.data_source_name.value
393
398
  parameters['path_structure'] = session.config.path_structure.model_dump()
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "patkit"
4
- version = "0.22.2"
4
+ version = "0.22.3"
5
5
  description = "Phonetic Analysis ToolKit: Tools for processing phonetic data"
6
6
  authors = [
7
7
  {email = "giuthas@gmail.com"},
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes