revoxx 1.0.2__tar.gz → 1.1.1__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 (142) hide show
  1. {revoxx-1.0.2/revoxx.egg-info → revoxx-1.1.1}/PKG-INFO +15 -6
  2. {revoxx-1.0.2 → revoxx-1.1.1}/README.md +11 -3
  3. {revoxx-1.0.2 → revoxx-1.1.1}/pyproject.toml +5 -3
  4. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/app.py +31 -6
  5. revoxx-1.1.1/revoxx/audio/editor.py +291 -0
  6. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/player.py +265 -118
  7. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/queue_manager.py +61 -19
  8. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/recorder.py +57 -16
  9. revoxx-1.1.1/revoxx/audio/worker_state.py +20 -0
  10. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/constants.py +24 -2
  11. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/controllers/__init__.py +2 -0
  12. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/controllers/audio_controller.py +147 -8
  13. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/controllers/display_controller.py +50 -2
  14. revoxx-1.1.1/revoxx/controllers/edit_controller.py +441 -0
  15. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/doc/USER_GUIDE.md +48 -2
  16. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/resources/keyboard_shortcuts.txt +13 -0
  17. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/import_text_dialog.py +2 -1
  18. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/open_session_dialog.py +2 -1
  19. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/user_guide_dialog.py +114 -13
  20. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/utterance_list_base.py +2 -1
  21. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/__init__.py +4 -1
  22. revoxx-1.1.1/revoxx/ui/spectrogram/controllers/playback_controller.py +224 -0
  23. revoxx-1.1.1/revoxx/ui/spectrogram/controllers/selection_visualizer.py +374 -0
  24. revoxx-1.1.1/revoxx/ui/spectrogram/playback_handler.py +650 -0
  25. revoxx-1.1.1/revoxx/ui/spectrogram/selection_interaction.py +408 -0
  26. revoxx-1.1.1/revoxx/ui/spectrogram/selection_state.py +139 -0
  27. revoxx-1.1.1/revoxx/ui/spectrogram/view_context.py +91 -0
  28. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/widget.py +303 -99
  29. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/window_base.py +1 -5
  30. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/window_factory.py +1 -2
  31. revoxx-1.1.1/revoxx/utils/adaptive_frame_rate.py +100 -0
  32. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/state.py +2 -2
  33. revoxx-1.1.1/revoxx/utils/tk_compat.py +43 -0
  34. {revoxx-1.0.2 → revoxx-1.1.1/revoxx.egg-info}/PKG-INFO +15 -6
  35. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx.egg-info/SOURCES.txt +9 -0
  36. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx.egg-info/requires.txt +4 -2
  37. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_audio_controller.py +9 -6
  38. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_audio_queue_manager.py +8 -2
  39. revoxx-1.0.2/revoxx/ui/spectrogram/controllers/playback_controller.py +0 -116
  40. revoxx-1.0.2/revoxx/ui/spectrogram/playback_handler.py +0 -458
  41. {revoxx-1.0.2 → revoxx-1.1.1}/LICENSE +0 -0
  42. {revoxx-1.0.2 → revoxx-1.1.1}/MANIFEST.in +0 -0
  43. {revoxx-1.0.2 → revoxx-1.1.1}/doc/import_raw_text.png +0 -0
  44. {revoxx-1.0.2 → revoxx-1.1.1}/doc/screenshot1.png +0 -0
  45. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/__init__.py +0 -0
  46. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/__main__.py +0 -0
  47. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/__init__.py +0 -0
  48. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/audio_buffer.py +0 -0
  49. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/audio_queue_processor.py +0 -0
  50. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/buffer_manager.py +0 -0
  51. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/level_calculator.py +0 -0
  52. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/processors/__init__.py +0 -0
  53. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/processors/clipping_detector.py +0 -0
  54. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/processors/mel_spectrogram.py +0 -0
  55. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/processors/processor_base.py +0 -0
  56. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/audio/shared_state.py +0 -0
  57. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/controllers/device_controller.py +0 -0
  58. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/controllers/dialog_controller.py +0 -0
  59. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/controllers/file_operations_controller.py +0 -0
  60. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/controllers/navigation_controller.py +0 -0
  61. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/controllers/process_manager.py +0 -0
  62. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/controllers/session_controller.py +0 -0
  63. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/dataset/__init__.py +0 -0
  64. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/dataset/exporter.py +0 -0
  65. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/resources/microphone.png +0 -0
  66. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/resources/templates/dataset_readme.txt +0 -0
  67. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/resources/templates/index_format_with_intensity.txt +0 -0
  68. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/resources/templates/index_format_without_intensity.txt +0 -0
  69. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/session/__init__.py +0 -0
  70. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/session/inspector.py +0 -0
  71. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/session/manager.py +0 -0
  72. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/session/models.py +0 -0
  73. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/session/script_parser.py +0 -0
  74. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/__init__.py +0 -0
  75. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/__init__.py +0 -0
  76. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/dataset_dialog.py +0 -0
  77. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/dialog_utils.py +0 -0
  78. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/find_dialog.py +0 -0
  79. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/help_dialog.py +0 -0
  80. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/new_session_dialog.py +0 -0
  81. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/progress_dialog.py +0 -0
  82. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/session_settings_dialog.py +0 -0
  83. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/dialogs/utterance_order_dialog.py +0 -0
  84. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/emotion_indicator.py +0 -0
  85. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/font_manager.py +0 -0
  86. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/frequency_axis.py +0 -0
  87. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/icon.py +0 -0
  88. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/info_overlay.py +0 -0
  89. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/level_meter/__init__.py +0 -0
  90. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/level_meter/config.py +0 -0
  91. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/level_meter/led_level_meter.py +0 -0
  92. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/menus/application_menu.py +0 -0
  93. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/menus/audio_devices.py +0 -0
  94. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/recording_display_state.py +0 -0
  95. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/__init__.py +0 -0
  96. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/clipping_visualizer.py +0 -0
  97. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/edge_indicator.py +0 -0
  98. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/zoom_controller.py +0 -0
  99. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/display_base.py +0 -0
  100. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/display_utils.py +0 -0
  101. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/mel_processor_manager.py +0 -0
  102. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/recording_display.py +0 -0
  103. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/spectrogram/recording_handler.py +0 -0
  104. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/style_config.py +0 -0
  105. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/themes.py +0 -0
  106. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/widget_initializer.py +0 -0
  107. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/ui/window_manager.py +0 -0
  108. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/__init__.py +0 -0
  109. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/active_recordings.py +0 -0
  110. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/audio_utils.py +0 -0
  111. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/config.py +0 -0
  112. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/device_manager.py +0 -0
  113. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/file_manager.py +0 -0
  114. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/process_cleanup.py +0 -0
  115. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/settings_manager.py +0 -0
  116. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/spectrogram_utils.py +0 -0
  117. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/text_importer.py +0 -0
  118. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx/utils/text_utils.py +0 -0
  119. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx.egg-info/dependency_links.txt +0 -0
  120. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx.egg-info/entry_points.txt +0 -0
  121. {revoxx-1.0.2 → revoxx-1.1.1}/revoxx.egg-info/top_level.txt +0 -0
  122. {revoxx-1.0.2 → revoxx-1.1.1}/scripts_module/__init__.py +0 -0
  123. {revoxx-1.0.2 → revoxx-1.1.1}/scripts_module/export.py +0 -0
  124. {revoxx-1.0.2 → revoxx-1.1.1}/scripts_module/vadiate.py +0 -0
  125. {revoxx-1.0.2 → revoxx-1.1.1}/setup.cfg +0 -0
  126. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_active_recordings.py +0 -0
  127. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_config.py +0 -0
  128. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_dataset_exporter.py +0 -0
  129. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_device_controller.py +0 -0
  130. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_dialog_controller.py +0 -0
  131. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_display_controller.py +0 -0
  132. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_file_manager.py +0 -0
  133. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_file_operations_controller.py +0 -0
  134. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_ipc_communication.py +0 -0
  135. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_navigation_controller.py +0 -0
  136. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_new_session_dialog.py +0 -0
  137. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_process_manager.py +0 -0
  138. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_session_controller.py +0 -0
  139. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_session_manager.py +0 -0
  140. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_session_models.py +0 -0
  141. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_stable_sorting.py +0 -0
  142. {revoxx-1.0.2 → revoxx-1.1.1}/tests/test_utterance_list_dialog_sorting.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: revoxx
3
- Version: 1.0.2
3
+ Version: 1.1.1
4
4
  Summary: Speech recording application for creating high-quality speech datasets
5
5
  Author-email: Grammatek ehf <info@grammatek.com>
6
6
  Maintainer-email: Grammatek ehf <info@grammatek.com>
@@ -32,13 +32,14 @@ Requires-Dist: sounddevice>=0.5.1
32
32
  Requires-Dist: soundfile>=0.12.0
33
33
  Requires-Dist: tqdm>=4.65.0
34
34
  Requires-Dist: markdown2>2.5.1
35
- Requires-Dist: tkinterweb>4.4.1
35
+ Provides-Extra: html
36
+ Requires-Dist: tkinterweb>4.4.1; extra == "html"
36
37
  Provides-Extra: vad
37
38
  Requires-Dist: torch>=2.0.0; extra == "vad"
38
39
  Requires-Dist: silero-vad>=5.0; extra == "vad"
39
40
  Requires-Dist: torchaudio<2.8.0; extra == "vad"
40
41
  Provides-Extra: dev
41
- Requires-Dist: black>=22.0.0; extra == "dev"
42
+ Requires-Dist: black<26,>=25.0.0; extra == "dev"
42
43
  Requires-Dist: isort>=5.10.0; extra == "dev"
43
44
  Requires-Dist: flake8>=6.0.0; extra == "dev"
44
45
  Requires-Dist: pytest>=7.0.0; extra == "dev"
@@ -50,14 +51,14 @@ Dynamic: license-file
50
51
 
51
52
  This repository provides **Revoxx**, a graphical recording application for recording raw speech and generating datasets.
52
53
 
53
- ![Version](https://img.shields.io/badge/Version-main-darkgreen)
54
+ [![PyPI version](https://img.shields.io/pypi/v/revoxx)](https://pypi.org/project/revoxx/)
54
55
  ![Python](https://img.shields.io/badge/python-3.9-blue?logo=python&logoColor=white)
55
56
  ![Python](https://img.shields.io/badge/python-3.10-blue?logo=python&logoColor=white)
56
57
  ![Python](https://img.shields.io/badge/python-3.11-blue?logo=python&logoColor=white)
57
58
  ![Python](https://img.shields.io/badge/python-3.12-blue?logo=python&logoColor=white)
58
59
  ![Python](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)
59
60
  [![CI Status](https://github.com/icelandic-lt/revoxx/actions/workflows/build.yml/badge.svg)](https://github.com/icelandic-lt/revoxx/actions/workflows/build.yml)
60
- ![Docker](https://img.shields.io/badge/Docker-[unavailable]-red)
61
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/icelandic-lt/revoxx)
61
62
 
62
63
  ## Overview
63
64
 
@@ -114,6 +115,12 @@ the Icelandic emotional speech dataset, and created this tool to minimize hassle
114
115
  - **Real-time monitoring** including toggable recording levels, mel spectrograms, maximum frequency detection, and more
115
116
  - Customizable **industry-standard presets for Peak/RMS levels**
116
117
  - Dedicated **Monitoring mode** for precise input calibration
118
+ - **Audio Editing** directly in the spectrogram view
119
+ - Set position markers to play from any point in the recording
120
+ - Create selection ranges for partial playback
121
+ - Delete ranges with automatic crossfade
122
+ - Insert new audio at marker position
123
+ - Replace selected ranges with new recordings
117
124
  - **Multi-Screen Support**
118
125
  - You can use multiple monitors to **separate recording view from speaker view**
119
126
  - We support Apple's [Sidecar](https://support.apple.com/en-us/102597) feature for a **convenient dual screen setup with an external iPad**
@@ -244,12 +251,14 @@ pip install -e .[dev,vad]
244
251
  ```
245
252
 
246
253
  Development dependencies include:
247
- - **black**: Code formatter
254
+ - **black**: Code formatter (pinned to 25.x for Python 3.9 compatibility)
248
255
  - **isort**: Import statement organizer
249
256
  - **flake8**: Code linter
250
257
  - **pytest**: Testing framework
251
258
  - **pytest-cov**: Code coverage reporting
252
259
 
260
+ > **Note**: Black is pinned to version 25.x because Black 26+ requires Python 3.10+ and introduces the "2026 stable style" with different formatting rules. This ensures consistent formatting across all supported Python versions (3.9-3.13).
261
+
253
262
  ### Running code quality checks
254
263
 
255
264
  ```bash
@@ -2,14 +2,14 @@
2
2
 
3
3
  This repository provides **Revoxx**, a graphical recording application for recording raw speech and generating datasets.
4
4
 
5
- ![Version](https://img.shields.io/badge/Version-main-darkgreen)
5
+ [![PyPI version](https://img.shields.io/pypi/v/revoxx)](https://pypi.org/project/revoxx/)
6
6
  ![Python](https://img.shields.io/badge/python-3.9-blue?logo=python&logoColor=white)
7
7
  ![Python](https://img.shields.io/badge/python-3.10-blue?logo=python&logoColor=white)
8
8
  ![Python](https://img.shields.io/badge/python-3.11-blue?logo=python&logoColor=white)
9
9
  ![Python](https://img.shields.io/badge/python-3.12-blue?logo=python&logoColor=white)
10
10
  ![Python](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)
11
11
  [![CI Status](https://github.com/icelandic-lt/revoxx/actions/workflows/build.yml/badge.svg)](https://github.com/icelandic-lt/revoxx/actions/workflows/build.yml)
12
- ![Docker](https://img.shields.io/badge/Docker-[unavailable]-red)
12
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/icelandic-lt/revoxx)
13
13
 
14
14
  ## Overview
15
15
 
@@ -66,6 +66,12 @@ the Icelandic emotional speech dataset, and created this tool to minimize hassle
66
66
  - **Real-time monitoring** including toggable recording levels, mel spectrograms, maximum frequency detection, and more
67
67
  - Customizable **industry-standard presets for Peak/RMS levels**
68
68
  - Dedicated **Monitoring mode** for precise input calibration
69
+ - **Audio Editing** directly in the spectrogram view
70
+ - Set position markers to play from any point in the recording
71
+ - Create selection ranges for partial playback
72
+ - Delete ranges with automatic crossfade
73
+ - Insert new audio at marker position
74
+ - Replace selected ranges with new recordings
69
75
  - **Multi-Screen Support**
70
76
  - You can use multiple monitors to **separate recording view from speaker view**
71
77
  - We support Apple's [Sidecar](https://support.apple.com/en-us/102597) feature for a **convenient dual screen setup with an external iPad**
@@ -196,12 +202,14 @@ pip install -e .[dev,vad]
196
202
  ```
197
203
 
198
204
  Development dependencies include:
199
- - **black**: Code formatter
205
+ - **black**: Code formatter (pinned to 25.x for Python 3.9 compatibility)
200
206
  - **isort**: Import statement organizer
201
207
  - **flake8**: Code linter
202
208
  - **pytest**: Testing framework
203
209
  - **pytest-cov**: Code coverage reporting
204
210
 
211
+ > **Note**: Black is pinned to version 25.x because Black 26+ requires Python 3.10+ and introduces the "2026 stable style" with different formatting rules. This ensures consistent formatting across all supported Python versions (3.9-3.13).
212
+
205
213
  ### Running code quality checks
206
214
 
207
215
  ```bash
@@ -38,10 +38,12 @@ dependencies = [
38
38
  "soundfile>=0.12.0",
39
39
  "tqdm>=4.65.0",
40
40
  "markdown2>2.5.1", # User guide dependencies
41
- "tkinterweb>4.4.1"
42
41
  ]
43
42
 
44
43
  [project.optional-dependencies]
44
+ html = [
45
+ "tkinterweb>4.4.1" # Requires Tkhtml - may need manual compilation for Tcl/Tk 9
46
+ ]
45
47
  vad = [
46
48
  "torch>=2.0.0",
47
49
  "silero-vad>=5.0",
@@ -51,7 +53,7 @@ vad = [
51
53
  # pip install torch --index-url https://download.pytorch.org/whl/cpu
52
54
  # pip install revoxx[vad]
53
55
  dev = [
54
- "black>=22.0.0",
56
+ "black>=25.0.0,<26",
55
57
  "isort>=5.10.0",
56
58
  "flake8>=6.0.0",
57
59
  "pytest>=7.0.0",
@@ -87,7 +89,7 @@ revoxx = [
87
89
 
88
90
  [tool.black]
89
91
  line-length = 88
90
- target-version = ['py39', 'py310', 'py311']
92
+ target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
91
93
  include = '\.pyi?$'
92
94
  extend-exclude = '''
93
95
  (
@@ -11,7 +11,7 @@ from pathlib import Path
11
11
  from typing import Optional
12
12
  import traceback
13
13
 
14
- from .constants import KeyBindings, FileConstants, MsgType
14
+ from .constants import KeyBindings, FileConstants, MsgType, UIConstants
15
15
  from .utils.config import RecorderConfig, load_config
16
16
  from .utils.state import AppState
17
17
  from .utils.file_manager import RecordingFileManager, ScriptFileManager
@@ -29,6 +29,7 @@ from .session import SessionManager, Session
29
29
  # Import all controllers
30
30
  from .controllers import (
31
31
  AudioController,
32
+ EditController,
32
33
  NavigationController,
33
34
  SessionController,
34
35
  DeviceController,
@@ -168,8 +169,6 @@ class Revoxx:
168
169
  theme_manager.set_theme(ThemePreset.CYAN)
169
170
 
170
171
  # Refresh UI constants with theme colors
171
- from .constants import UIConstants
172
-
173
172
  UIConstants.refresh()
174
173
 
175
174
  # Initialize WindowManager
@@ -239,6 +238,7 @@ class Revoxx:
239
238
  self.display_controller = DisplayController(self, self.window_manager)
240
239
  self.file_operations_controller = FileOperationsController(self)
241
240
  self.dialog_controller = DialogController(self)
241
+ self.edit_controller = EditController(self)
242
242
 
243
243
  def _populate_app_callbacks(self):
244
244
  """Populate app_callbacks dictionary with controller methods."""
@@ -308,13 +308,17 @@ class Revoxx:
308
308
  self.window.window.bind(
309
309
  f"<{KeyBindings.PLAY}>", lambda e: self.audio_controller.play_current()
310
310
  )
311
+ self.window.window.bind(
312
+ f"<{KeyBindings.STOP}>",
313
+ lambda e: self.audio_controller.stop_all_playback_activities(),
314
+ )
311
315
  self.window.window.bind(
312
316
  "<Control-d>",
313
- lambda e: self.file_operations_controller.delete_current_recording(),
317
+ lambda e: self._handle_delete(),
314
318
  )
315
319
  self.window.window.bind(
316
320
  "<Control-D>",
317
- lambda e: self.file_operations_controller.delete_current_recording(),
321
+ lambda e: self._handle_delete(),
318
322
  )
319
323
 
320
324
  # Navigation keys
@@ -379,7 +383,7 @@ class Revoxx:
379
383
  modifier = "Control"
380
384
  self.window.window.bind(
381
385
  f"<{modifier}-{KeyBindings.DELETE_RECORDING}>",
382
- lambda e: self.file_operations_controller.delete_current_recording(),
386
+ lambda e: self._handle_delete(),
383
387
  )
384
388
 
385
389
  # Help and info
@@ -391,6 +395,11 @@ class Revoxx:
391
395
  lambda e: self.display_controller.toggle_info_panel(),
392
396
  )
393
397
 
398
+ # Clear selection with Escape
399
+ self.window.window.bind(
400
+ "<Escape>", lambda e: self._clear_spectrogram_selection()
401
+ )
402
+
394
403
  # Second window shortcuts (Shift + key)
395
404
  self.window.window.bind(
396
405
  "<Shift-M>",
@@ -594,6 +603,22 @@ class Revoxx:
594
603
  # Exit
595
604
  sys.exit(0)
596
605
 
606
+ def _clear_spectrogram_selection(self) -> None:
607
+ """Clear marker and selection in spectrogram."""
608
+ if self.window and self.window.mel_spectrogram:
609
+ self.window.mel_spectrogram.clear_selection()
610
+
611
+ def _handle_delete(self) -> None:
612
+ """Handle delete action - deletes selection if active, otherwise recording."""
613
+ if (
614
+ self.window
615
+ and self.window.mel_spectrogram
616
+ and self.window.mel_spectrogram.selection_state.has_selection
617
+ ):
618
+ self.edit_controller.delete_selection()
619
+ else:
620
+ self.file_operations_controller.delete_current_recording()
621
+
597
622
  def _toggle_fullscreen(self):
598
623
  """Toggle fullscreen mode.
599
624
  This setting is saved to the user's settings.
@@ -0,0 +1,291 @@
1
+ """Audio editing operations with cross-fade support.
2
+
3
+ This module provides audio editing functions for deleting, inserting,
4
+ and replacing audio segments with smooth cross-fade transitions.
5
+ """
6
+
7
+ import numpy as np
8
+
9
+ from ..constants import AudioConstants
10
+
11
+
12
+ class AudioEditor:
13
+ """Provides audio editing operations with cross-fade support.
14
+
15
+ All operations use equal-power cross-fading to ensure smooth
16
+ transitions without audible clicks or pops.
17
+ """
18
+
19
+ @staticmethod
20
+ def delete_range(
21
+ audio: np.ndarray, start_sample: int, end_sample: int, sample_rate: int
22
+ ) -> np.ndarray:
23
+ """Delete a range of audio samples with cross-fade.
24
+
25
+ Args:
26
+ audio: Input audio array
27
+ start_sample: Start of deletion range
28
+ end_sample: End of deletion range
29
+ sample_rate: Audio sample rate in Hz
30
+
31
+ Returns:
32
+ New audio array with the range deleted and cross-faded
33
+ """
34
+ if start_sample >= end_sample:
35
+ return audio.copy()
36
+
37
+ if start_sample < 0:
38
+ start_sample = 0
39
+ if end_sample > len(audio):
40
+ end_sample = len(audio)
41
+
42
+ # Calculate fade samples
43
+ selection_samples = end_sample - start_sample
44
+ fade_samples = AudioEditor._calculate_fade_samples(
45
+ sample_rate, selection_samples
46
+ )
47
+
48
+ # Get the parts before and after the deletion
49
+ before = audio[:start_sample]
50
+ after = audio[end_sample:]
51
+
52
+ if (
53
+ fade_samples > 0
54
+ and len(before) >= fade_samples
55
+ and len(after) >= fade_samples
56
+ ):
57
+ # Apply cross-fade between the end of 'before' and start of 'after'
58
+ before_fade = before[-fade_samples:]
59
+ after_fade = after[:fade_samples]
60
+
61
+ crossfaded = AudioEditor._equal_power_crossfade(
62
+ before_fade, after_fade, fade_samples
63
+ )
64
+
65
+ # Construct result: before (minus fade region) + crossfade + after (minus fade region)
66
+ result = np.concatenate(
67
+ [before[:-fade_samples], crossfaded, after[fade_samples:]]
68
+ )
69
+ else:
70
+ # No cross-fade possible, just concatenate
71
+ result = np.concatenate([before, after])
72
+
73
+ return result
74
+
75
+ @staticmethod
76
+ def insert_at_position(
77
+ original: np.ndarray,
78
+ insert: np.ndarray,
79
+ position: int,
80
+ sample_rate: int,
81
+ ) -> np.ndarray:
82
+ """Insert audio at a position with cross-fade.
83
+
84
+ Args:
85
+ original: Original audio array
86
+ insert: Audio to insert
87
+ position: Sample position to insert at
88
+ sample_rate: Audio sample rate in Hz
89
+
90
+ Returns:
91
+ New audio array with the inserted content cross-faded
92
+ """
93
+ if len(insert) == 0:
94
+ return original.copy()
95
+
96
+ if position < 0:
97
+ position = 0
98
+ if position > len(original):
99
+ position = len(original)
100
+
101
+ # Calculate fade samples based on insert length
102
+ fade_samples = AudioEditor._calculate_fade_samples(sample_rate, len(insert))
103
+
104
+ before = original[:position]
105
+ after = original[position:]
106
+
107
+ if fade_samples > 0:
108
+ # Cross-fade at insertion point
109
+ result_parts = []
110
+
111
+ # Before section
112
+ if len(before) >= fade_samples:
113
+ # Fade out the end of 'before' and fade in start of 'insert'
114
+ before_fade = before[-fade_samples:]
115
+ insert_start_fade = (
116
+ insert[:fade_samples] if len(insert) >= fade_samples else insert
117
+ )
118
+
119
+ if len(insert_start_fade) == fade_samples:
120
+ crossfaded_start = AudioEditor._equal_power_crossfade(
121
+ before_fade, insert_start_fade, fade_samples
122
+ )
123
+ result_parts.append(before[:-fade_samples])
124
+ result_parts.append(crossfaded_start)
125
+ else:
126
+ result_parts.append(before)
127
+ else:
128
+ result_parts.append(before)
129
+
130
+ # Middle section of insert (if any)
131
+ if len(insert) > 2 * fade_samples:
132
+ result_parts.append(insert[fade_samples:-fade_samples])
133
+ elif len(insert) > fade_samples:
134
+ result_parts.append(insert[fade_samples:])
135
+
136
+ # After section
137
+ if len(after) >= fade_samples and len(insert) >= fade_samples:
138
+ # Fade out end of 'insert' and fade in start of 'after'
139
+ insert_end_fade = insert[-fade_samples:]
140
+ after_fade = after[:fade_samples]
141
+
142
+ crossfaded_end = AudioEditor._equal_power_crossfade(
143
+ insert_end_fade, after_fade, fade_samples
144
+ )
145
+ result_parts.append(crossfaded_end)
146
+ result_parts.append(after[fade_samples:])
147
+ else:
148
+ result_parts.append(after)
149
+
150
+ result = np.concatenate([p for p in result_parts if len(p) > 0])
151
+ else:
152
+ # No cross-fade, simple concatenation
153
+ result = np.concatenate([before, insert, after])
154
+
155
+ return result
156
+
157
+ @staticmethod
158
+ def replace_range(
159
+ original: np.ndarray,
160
+ replacement: np.ndarray,
161
+ start_sample: int,
162
+ end_sample: int,
163
+ sample_rate: int,
164
+ ) -> np.ndarray:
165
+ """Replace a range of audio with new content and cross-fade.
166
+
167
+ Args:
168
+ original: Original audio array
169
+ replacement: Audio to replace the range with
170
+ start_sample: Start of range to replace
171
+ end_sample: End of range to replace
172
+ sample_rate: Audio sample rate in Hz
173
+
174
+ Returns:
175
+ New audio array with the range replaced and cross-faded
176
+ """
177
+ if start_sample >= end_sample:
178
+ return original.copy()
179
+
180
+ if start_sample < 0:
181
+ start_sample = 0
182
+ if end_sample > len(original):
183
+ end_sample = len(original)
184
+
185
+ # Calculate fade samples
186
+ selection_samples = end_sample - start_sample
187
+ fade_samples = AudioEditor._calculate_fade_samples(
188
+ sample_rate, min(selection_samples, len(replacement))
189
+ )
190
+
191
+ before = original[:start_sample]
192
+ after = original[end_sample:]
193
+
194
+ if fade_samples > 0:
195
+ result_parts = []
196
+
197
+ # Cross-fade at start of replacement
198
+ if len(before) >= fade_samples and len(replacement) >= fade_samples:
199
+ before_fade = before[-fade_samples:]
200
+ replacement_start = replacement[:fade_samples]
201
+
202
+ crossfaded_start = AudioEditor._equal_power_crossfade(
203
+ before_fade, replacement_start, fade_samples
204
+ )
205
+ result_parts.append(before[:-fade_samples])
206
+ result_parts.append(crossfaded_start)
207
+ else:
208
+ result_parts.append(before)
209
+
210
+ # Middle of replacement
211
+ if len(replacement) > 2 * fade_samples:
212
+ result_parts.append(replacement[fade_samples:-fade_samples])
213
+ elif len(replacement) > fade_samples:
214
+ result_parts.append(replacement[fade_samples:])
215
+
216
+ # Cross-fade at end of replacement
217
+ if len(after) >= fade_samples and len(replacement) >= fade_samples:
218
+ replacement_end = replacement[-fade_samples:]
219
+ after_start = after[:fade_samples]
220
+
221
+ crossfaded_end = AudioEditor._equal_power_crossfade(
222
+ replacement_end, after_start, fade_samples
223
+ )
224
+ result_parts.append(crossfaded_end)
225
+ result_parts.append(after[fade_samples:])
226
+ else:
227
+ result_parts.append(after)
228
+
229
+ result = np.concatenate([p for p in result_parts if len(p) > 0])
230
+ else:
231
+ # No cross-fade, simple replacement
232
+ result = np.concatenate([before, replacement, after])
233
+
234
+ return result
235
+
236
+ @staticmethod
237
+ def _equal_power_crossfade(
238
+ audio_a: np.ndarray, audio_b: np.ndarray, fade_samples: int
239
+ ) -> np.ndarray:
240
+ """Apply equal-power cross-fade between two audio segments.
241
+
242
+ Equal-power cross-fade maintains constant perceived loudness
243
+ during the transition by using sine/cosine curves for gain.
244
+
245
+ Args:
246
+ audio_a: First audio segment (fade out)
247
+ audio_b: Second audio segment (fade in)
248
+ fade_samples: Number of samples for the fade
249
+
250
+ Returns:
251
+ Cross-faded audio segment of length fade_samples
252
+ """
253
+ if fade_samples <= 0:
254
+ return audio_b[:0] if len(audio_b) > 0 else np.array([])
255
+
256
+ # Ensure we have enough samples
257
+ actual_samples = min(fade_samples, len(audio_a), len(audio_b))
258
+ if actual_samples <= 0:
259
+ return np.array([])
260
+
261
+ # Equal power cross-fade using sine/cosine curves
262
+ t = np.linspace(0, np.pi / 2, actual_samples)
263
+ gain_a = np.cos(t) # Fade out
264
+ gain_b = np.sin(t) # Fade in
265
+
266
+ result = audio_a[:actual_samples] * gain_a + audio_b[:actual_samples] * gain_b
267
+
268
+ return result
269
+
270
+ @staticmethod
271
+ def _calculate_fade_samples(sample_rate: int, selection_samples: int) -> int:
272
+ """Calculate the number of samples for cross-fade.
273
+
274
+ The fade length is adaptive: it uses the configured cross-fade duration
275
+ but is capped at half the selection length to ensure smooth transitions
276
+ for short selections.
277
+
278
+ Args:
279
+ sample_rate: Audio sample rate in Hz
280
+ selection_samples: Number of samples in the selection
281
+
282
+ Returns:
283
+ Number of samples to use for cross-fade
284
+ """
285
+ # Calculate fade samples from configured duration
286
+ fade_from_config = int(AudioConstants.CROSSFADE_MS * sample_rate / 1000)
287
+
288
+ # Cap at half the selection length
289
+ max_fade = selection_samples // 2
290
+
291
+ return min(fade_from_config, max_fade)