revoxx 1.1.1__tar.gz → 1.1.2__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.
- {revoxx-1.1.1/revoxx.egg-info → revoxx-1.1.2}/PKG-INFO +2 -1
- {revoxx-1.1.1 → revoxx-1.1.2}/README.md +1 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/app.py +20 -0
- revoxx-1.1.2/revoxx/audio/edit_commands.py +338 -0
- revoxx-1.1.2/revoxx/audio/undo_stack.py +170 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/controllers/edit_controller.py +209 -20
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/controllers/file_operations_controller.py +12 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/controllers/navigation_controller.py +6 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/doc/USER_GUIDE.md +24 -12
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/resources/keyboard_shortcuts.txt +2 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/menus/application_menu.py +29 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/file_manager.py +57 -0
- {revoxx-1.1.1 → revoxx-1.1.2/revoxx.egg-info}/PKG-INFO +2 -1
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx.egg-info/SOURCES.txt +2 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/LICENSE +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/MANIFEST.in +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/doc/import_raw_text.png +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/doc/screenshot1.png +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/pyproject.toml +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/__main__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/audio_buffer.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/audio_queue_processor.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/buffer_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/editor.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/level_calculator.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/player.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/processors/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/processors/clipping_detector.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/processors/mel_spectrogram.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/processors/processor_base.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/queue_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/recorder.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/shared_state.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/audio/worker_state.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/constants.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/controllers/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/controllers/audio_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/controllers/device_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/controllers/dialog_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/controllers/display_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/controllers/process_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/controllers/session_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/dataset/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/dataset/exporter.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/resources/microphone.png +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/resources/templates/dataset_readme.txt +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/resources/templates/index_format_with_intensity.txt +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/resources/templates/index_format_without_intensity.txt +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/session/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/session/inspector.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/session/manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/session/models.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/session/script_parser.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/dataset_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/dialog_utils.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/find_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/help_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/import_text_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/new_session_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/open_session_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/progress_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/session_settings_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/user_guide_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/utterance_list_base.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/dialogs/utterance_order_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/emotion_indicator.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/font_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/frequency_axis.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/icon.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/info_overlay.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/level_meter/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/level_meter/config.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/level_meter/led_level_meter.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/menus/audio_devices.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/recording_display_state.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/controllers/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/controllers/clipping_visualizer.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/controllers/edge_indicator.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/controllers/playback_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/controllers/selection_visualizer.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/controllers/zoom_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/display_base.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/display_utils.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/mel_processor_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/playback_handler.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/recording_display.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/recording_handler.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/selection_interaction.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/selection_state.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/view_context.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/spectrogram/widget.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/style_config.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/themes.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/widget_initializer.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/window_base.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/window_factory.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/ui/window_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/active_recordings.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/adaptive_frame_rate.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/audio_utils.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/config.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/device_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/process_cleanup.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/settings_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/spectrogram_utils.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/state.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/text_importer.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/text_utils.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx/utils/tk_compat.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx.egg-info/dependency_links.txt +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx.egg-info/entry_points.txt +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx.egg-info/requires.txt +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/revoxx.egg-info/top_level.txt +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/scripts_module/__init__.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/scripts_module/export.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/scripts_module/vadiate.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/setup.cfg +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_active_recordings.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_audio_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_audio_queue_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_config.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_dataset_exporter.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_device_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_dialog_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_display_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_file_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_file_operations_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_ipc_communication.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_navigation_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_new_session_dialog.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_process_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_session_controller.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_session_manager.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_session_models.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/tests/test_stable_sorting.py +0 -0
- {revoxx-1.1.1 → revoxx-1.1.2}/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.1.
|
|
3
|
+
Version: 1.1.2
|
|
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>
|
|
@@ -121,6 +121,7 @@ the Icelandic emotional speech dataset, and created this tool to minimize hassle
|
|
|
121
121
|
- Delete ranges with automatic crossfade
|
|
122
122
|
- Insert new audio at marker position
|
|
123
123
|
- Replace selected ranges with new recordings
|
|
124
|
+
- Multiple undo/redo for all editing operations
|
|
124
125
|
- **Multi-Screen Support**
|
|
125
126
|
- You can use multiple monitors to **separate recording view from speaker view**
|
|
126
127
|
- We support Apple's [Sidecar](https://support.apple.com/en-us/102597) feature for a **convenient dual screen setup with an external iPad**
|
|
@@ -72,6 +72,7 @@ the Icelandic emotional speech dataset, and created this tool to minimize hassle
|
|
|
72
72
|
- Delete ranges with automatic crossfade
|
|
73
73
|
- Insert new audio at marker position
|
|
74
74
|
- Replace selected ranges with new recordings
|
|
75
|
+
- Multiple undo/redo for all editing operations
|
|
75
76
|
- **Multi-Screen Support**
|
|
76
77
|
- You can use multiple monitors to **separate recording view from speaker view**
|
|
77
78
|
- We support Apple's [Sidecar](https://support.apple.com/en-us/102597) feature for a **convenient dual screen setup with an external iPad**
|
|
@@ -445,12 +445,22 @@ class Revoxx:
|
|
|
445
445
|
self.window.window.bind("<Command-Q>", lambda e: self._handle_cmd_q())
|
|
446
446
|
# Also try to catch it with createcommand
|
|
447
447
|
self.window.window.createcommand("::tk::mac::Quit", self._handle_cmd_q)
|
|
448
|
+
# Undo/Redo
|
|
449
|
+
self.window.window.bind("<Command-z>", lambda e: self._undo())
|
|
450
|
+
self.window.window.bind("<Command-Z>", lambda e: self._undo())
|
|
451
|
+
self.window.window.bind("<Shift-Command-z>", lambda e: self._redo())
|
|
452
|
+
self.window.window.bind("<Shift-Command-Z>", lambda e: self._redo())
|
|
448
453
|
|
|
449
454
|
# Session keys
|
|
450
455
|
self.window.window.bind("<Control-n>", lambda e: self._new_session())
|
|
451
456
|
self.window.window.bind("<Control-N>", lambda e: self._new_session())
|
|
452
457
|
self.window.window.bind("<Control-o>", lambda e: self._open_session())
|
|
453
458
|
self.window.window.bind("<Control-O>", lambda e: self._open_session())
|
|
459
|
+
# Undo/Redo (Control for Linux/Windows)
|
|
460
|
+
self.window.window.bind("<Control-z>", lambda e: self._undo())
|
|
461
|
+
self.window.window.bind("<Control-Z>", lambda e: self._undo())
|
|
462
|
+
self.window.window.bind("<Shift-Control-z>", lambda e: self._redo())
|
|
463
|
+
self.window.window.bind("<Shift-Control-Z>", lambda e: self._redo())
|
|
454
464
|
|
|
455
465
|
# Window close event
|
|
456
466
|
self.window.window.protocol("WM_DELETE_WINDOW", self._quit)
|
|
@@ -534,6 +544,16 @@ class Revoxx:
|
|
|
534
544
|
self._quit()
|
|
535
545
|
return "break" # Prevent default handling
|
|
536
546
|
|
|
547
|
+
def _undo(self):
|
|
548
|
+
"""Handle undo keyboard shortcut."""
|
|
549
|
+
self.edit_controller.undo()
|
|
550
|
+
return "break"
|
|
551
|
+
|
|
552
|
+
def _redo(self):
|
|
553
|
+
"""Handle redo keyboard shortcut."""
|
|
554
|
+
self.edit_controller.redo()
|
|
555
|
+
return "break"
|
|
556
|
+
|
|
537
557
|
def _perform_cleanup(self):
|
|
538
558
|
"""Perform cleanup when signals are received or on emergency exit."""
|
|
539
559
|
# Only do critical cleanup - no UI interactions
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Edit commands for undo/redo support.
|
|
2
|
+
|
|
3
|
+
This module provides command classes that encapsulate audio editing operations.
|
|
4
|
+
Each command stores the data needed to undo the operation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Optional
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..files.file_manager import FileManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EditCommand(ABC):
|
|
18
|
+
"""Abstract base class for edit commands.
|
|
19
|
+
|
|
20
|
+
Each command represents an audio editing operation that can be
|
|
21
|
+
undone and redone. Commands store the data needed to reverse
|
|
22
|
+
the operation.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, filepath: Path, sample_rate: int, subtype: Optional[str] = None):
|
|
26
|
+
"""Initialize the command.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
filepath: Path to the audio file being edited
|
|
30
|
+
sample_rate: Sample rate of the audio
|
|
31
|
+
subtype: Audio subtype for saving (e.g., "PCM_16")
|
|
32
|
+
"""
|
|
33
|
+
self.filepath = filepath
|
|
34
|
+
self.sample_rate = sample_rate
|
|
35
|
+
self.subtype = subtype
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def execute(self, file_manager: "FileManager") -> bool:
|
|
39
|
+
"""Execute the command.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
file_manager: FileManager instance for audio I/O
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
True if successful, False otherwise
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def inverse(self) -> "EditCommand":
|
|
51
|
+
"""Create the inverse command for undo.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A command that reverses this operation
|
|
55
|
+
"""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def description(self) -> str:
|
|
60
|
+
"""Get a human-readable description of this command.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Description string (e.g., "Delete Range")
|
|
64
|
+
"""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AudioSnapshotCommand(EditCommand):
|
|
69
|
+
"""Command that stores complete audio snapshots for undo/redo.
|
|
70
|
+
|
|
71
|
+
This approach ensures exact restoration without re-applying cross-fade.
|
|
72
|
+
Stores both the audio before and after the operation.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
filepath: Path,
|
|
78
|
+
sample_rate: int,
|
|
79
|
+
audio_before: np.ndarray,
|
|
80
|
+
audio_after: np.ndarray,
|
|
81
|
+
subtype: Optional[str] = None,
|
|
82
|
+
selection_start_time: Optional[float] = None,
|
|
83
|
+
selection_end_time: Optional[float] = None,
|
|
84
|
+
marker_after_edit: Optional[float] = None,
|
|
85
|
+
operation_description: str = "Edit",
|
|
86
|
+
):
|
|
87
|
+
"""Initialize audio snapshot command.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
filepath: Path to the audio file
|
|
91
|
+
sample_rate: Sample rate of the audio
|
|
92
|
+
audio_before: Complete audio data before the operation
|
|
93
|
+
audio_after: Complete audio data after the operation
|
|
94
|
+
subtype: Audio subtype for saving
|
|
95
|
+
selection_start_time: Selection to restore on undo
|
|
96
|
+
selection_end_time: Selection to restore on undo
|
|
97
|
+
marker_after_edit: Marker to set after redo
|
|
98
|
+
operation_description: Description of the operation
|
|
99
|
+
"""
|
|
100
|
+
super().__init__(filepath, sample_rate, subtype)
|
|
101
|
+
self.audio_before = audio_before
|
|
102
|
+
self.audio_after = audio_after
|
|
103
|
+
self.selection_start_time = selection_start_time
|
|
104
|
+
self.selection_end_time = selection_end_time
|
|
105
|
+
self.marker_after_edit = marker_after_edit
|
|
106
|
+
self.operation_description = operation_description
|
|
107
|
+
|
|
108
|
+
def execute(self, file_manager: "FileManager") -> bool:
|
|
109
|
+
"""Write audio_after to file (for redo)."""
|
|
110
|
+
try:
|
|
111
|
+
file_manager.save_audio(
|
|
112
|
+
self.filepath, self.audio_after, self.sample_rate, self.subtype
|
|
113
|
+
)
|
|
114
|
+
return True
|
|
115
|
+
except (OSError, ValueError):
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
def inverse(self) -> "AudioSnapshotCommand":
|
|
119
|
+
"""Create inverse command that restores audio_before."""
|
|
120
|
+
return AudioSnapshotCommand(
|
|
121
|
+
filepath=self.filepath,
|
|
122
|
+
sample_rate=self.sample_rate,
|
|
123
|
+
audio_before=self.audio_after,
|
|
124
|
+
audio_after=self.audio_before,
|
|
125
|
+
subtype=self.subtype,
|
|
126
|
+
selection_start_time=self.selection_start_time,
|
|
127
|
+
selection_end_time=self.selection_end_time,
|
|
128
|
+
marker_after_edit=None,
|
|
129
|
+
operation_description=self.operation_description,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def description(self) -> str:
|
|
133
|
+
"""Get description of this command."""
|
|
134
|
+
return self.operation_description
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Convenience aliases for specific operations (all use AudioSnapshotCommand internally)
|
|
138
|
+
DeleteRangeCommand = AudioSnapshotCommand
|
|
139
|
+
InsertCommand = AudioSnapshotCommand
|
|
140
|
+
ReplaceRangeCommand = AudioSnapshotCommand
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TrashClipCommand(EditCommand):
|
|
144
|
+
"""Command for moving a clip to trash.
|
|
145
|
+
|
|
146
|
+
Uses the existing trash system (move_to_trash/restore_from_trash).
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
filepath: Path,
|
|
152
|
+
sample_rate: int,
|
|
153
|
+
label: str,
|
|
154
|
+
take: int,
|
|
155
|
+
):
|
|
156
|
+
"""Initialize trash clip command.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
filepath: Path to the audio file
|
|
160
|
+
sample_rate: Sample rate of the audio
|
|
161
|
+
label: Script label/ID for the utterance
|
|
162
|
+
take: Take number
|
|
163
|
+
"""
|
|
164
|
+
super().__init__(filepath, sample_rate, None)
|
|
165
|
+
self.label = label
|
|
166
|
+
self.take = take
|
|
167
|
+
|
|
168
|
+
def execute(self, file_manager: "FileManager") -> bool:
|
|
169
|
+
"""Move the clip to trash."""
|
|
170
|
+
return file_manager.move_to_trash(self.label, self.take)
|
|
171
|
+
|
|
172
|
+
def inverse(self) -> "RestoreFromTrashCommand":
|
|
173
|
+
"""Create a restore command to undo this deletion."""
|
|
174
|
+
return RestoreFromTrashCommand(
|
|
175
|
+
filepath=self.filepath,
|
|
176
|
+
sample_rate=self.sample_rate,
|
|
177
|
+
label=self.label,
|
|
178
|
+
take=self.take,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def description(self) -> str:
|
|
182
|
+
"""Get description of this command."""
|
|
183
|
+
return f"Delete Clip (take {self.take})"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class RestoreFromTrashCommand(EditCommand):
|
|
187
|
+
"""Command for restoring a clip from trash."""
|
|
188
|
+
|
|
189
|
+
def __init__(
|
|
190
|
+
self,
|
|
191
|
+
filepath: Path,
|
|
192
|
+
sample_rate: int,
|
|
193
|
+
label: str,
|
|
194
|
+
take: int,
|
|
195
|
+
):
|
|
196
|
+
"""Initialize restore from trash command."""
|
|
197
|
+
super().__init__(filepath, sample_rate, None)
|
|
198
|
+
self.label = label
|
|
199
|
+
self.take = take
|
|
200
|
+
|
|
201
|
+
def execute(self, file_manager: "FileManager") -> bool:
|
|
202
|
+
"""Restore the clip from trash."""
|
|
203
|
+
return file_manager.restore_from_trash(self.label, self.take)
|
|
204
|
+
|
|
205
|
+
def inverse(self) -> "TrashClipCommand":
|
|
206
|
+
"""Create a trash command to undo this restoration."""
|
|
207
|
+
return TrashClipCommand(
|
|
208
|
+
filepath=self.filepath,
|
|
209
|
+
sample_rate=self.sample_rate,
|
|
210
|
+
label=self.label,
|
|
211
|
+
take=self.take,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def description(self) -> str:
|
|
215
|
+
"""Get description of this command."""
|
|
216
|
+
return f"Restore Clip (take {self.take})"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class DeleteClipCommand(EditCommand):
|
|
220
|
+
"""Command for deleting an entire audio clip.
|
|
221
|
+
|
|
222
|
+
Stores the complete audio data so it can be restored on undo.
|
|
223
|
+
Note: For trash-based deletion, use TrashClipCommand instead.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def __init__(
|
|
227
|
+
self,
|
|
228
|
+
filepath: Path,
|
|
229
|
+
sample_rate: int,
|
|
230
|
+
audio_data: np.ndarray,
|
|
231
|
+
subtype: Optional[str] = None,
|
|
232
|
+
):
|
|
233
|
+
"""Initialize delete clip command.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
filepath: Path to the audio file
|
|
237
|
+
sample_rate: Sample rate of the audio
|
|
238
|
+
audio_data: The complete audio data of the deleted clip
|
|
239
|
+
subtype: Audio subtype for saving (e.g., "PCM_16")
|
|
240
|
+
"""
|
|
241
|
+
super().__init__(filepath, sample_rate)
|
|
242
|
+
self.audio_data = audio_data
|
|
243
|
+
self.subtype = subtype
|
|
244
|
+
|
|
245
|
+
def execute(self, file_manager: "FileManager") -> bool:
|
|
246
|
+
"""Execute the delete clip operation (delete the file).
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
file_manager: FileManager instance for audio I/O
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
True if successful, False otherwise
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
if self.filepath.exists():
|
|
256
|
+
self.filepath.unlink()
|
|
257
|
+
return True
|
|
258
|
+
except OSError:
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
def inverse(self) -> "RestoreClipCommand":
|
|
262
|
+
"""Create a restore command to undo this deletion.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
RestoreClipCommand that recreates the deleted file
|
|
266
|
+
"""
|
|
267
|
+
return RestoreClipCommand(
|
|
268
|
+
filepath=self.filepath,
|
|
269
|
+
sample_rate=self.sample_rate,
|
|
270
|
+
audio_data=self.audio_data,
|
|
271
|
+
subtype=self.subtype,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def description(self) -> str:
|
|
275
|
+
"""Get description of this command."""
|
|
276
|
+
duration = len(self.audio_data) / self.sample_rate
|
|
277
|
+
return f"Delete Clip ({duration:.2f}s)"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class RestoreClipCommand(EditCommand):
|
|
281
|
+
"""Command for restoring a deleted audio clip.
|
|
282
|
+
|
|
283
|
+
Used as the inverse of DeleteClipCommand.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
def __init__(
|
|
287
|
+
self,
|
|
288
|
+
filepath: Path,
|
|
289
|
+
sample_rate: int,
|
|
290
|
+
audio_data: np.ndarray,
|
|
291
|
+
subtype: Optional[str] = None,
|
|
292
|
+
):
|
|
293
|
+
"""Initialize restore clip command.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
filepath: Path to the audio file
|
|
297
|
+
sample_rate: Sample rate of the audio
|
|
298
|
+
audio_data: The audio data to restore
|
|
299
|
+
subtype: Audio subtype for saving (e.g., "PCM_16")
|
|
300
|
+
"""
|
|
301
|
+
super().__init__(filepath, sample_rate)
|
|
302
|
+
self.audio_data = audio_data
|
|
303
|
+
self.subtype = subtype
|
|
304
|
+
|
|
305
|
+
def execute(self, file_manager: "FileManager") -> bool:
|
|
306
|
+
"""Execute the restore operation (recreate the file).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
file_manager: FileManager instance for audio I/O
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
True if successful, False otherwise
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
file_manager.save_audio(
|
|
316
|
+
self.filepath, self.audio_data, self.sample_rate, self.subtype
|
|
317
|
+
)
|
|
318
|
+
return True
|
|
319
|
+
except (OSError, ValueError):
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
def inverse(self) -> "DeleteClipCommand":
|
|
323
|
+
"""Create a delete command to undo this restoration.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
DeleteClipCommand that deletes the file again
|
|
327
|
+
"""
|
|
328
|
+
return DeleteClipCommand(
|
|
329
|
+
filepath=self.filepath,
|
|
330
|
+
sample_rate=self.sample_rate,
|
|
331
|
+
audio_data=self.audio_data,
|
|
332
|
+
subtype=self.subtype,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def description(self) -> str:
|
|
336
|
+
"""Get description of this command."""
|
|
337
|
+
duration = len(self.audio_data) / self.sample_rate
|
|
338
|
+
return f"Restore Clip ({duration:.2f}s)"
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Undo/redo stack for audio editing operations.
|
|
2
|
+
|
|
3
|
+
This module provides the UndoStack class that manages undo and redo
|
|
4
|
+
operations using the command pattern.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
8
|
+
|
|
9
|
+
from .edit_commands import EditCommand
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..files.file_manager import FileManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UndoStack:
|
|
16
|
+
"""Manages undo and redo stacks for edit commands.
|
|
17
|
+
|
|
18
|
+
Uses two stacks:
|
|
19
|
+
- Undo stack: Commands that can be undone (most recent on top)
|
|
20
|
+
- Redo stack: Commands that were undone and can be redone
|
|
21
|
+
|
|
22
|
+
When a new command is pushed, the redo stack is cleared.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, max_size: int = 50):
|
|
26
|
+
"""Initialize the undo stack.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
max_size: Maximum number of commands to keep in the undo stack.
|
|
30
|
+
Older commands are discarded when the limit is reached.
|
|
31
|
+
"""
|
|
32
|
+
self._max_size = max_size
|
|
33
|
+
self._undo_stack: List[EditCommand] = []
|
|
34
|
+
self._redo_stack: List[EditCommand] = []
|
|
35
|
+
|
|
36
|
+
def push(self, command: EditCommand) -> None:
|
|
37
|
+
"""Push a new command onto the undo stack.
|
|
38
|
+
|
|
39
|
+
This clears the redo stack since a new action invalidates
|
|
40
|
+
any previously undone commands.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
command: The command that was just executed
|
|
44
|
+
"""
|
|
45
|
+
self._undo_stack.append(command)
|
|
46
|
+
self._redo_stack.clear()
|
|
47
|
+
|
|
48
|
+
if len(self._undo_stack) > self._max_size:
|
|
49
|
+
self._undo_stack.pop(0)
|
|
50
|
+
|
|
51
|
+
def undo(self, file_manager: "FileManager") -> Optional[EditCommand]:
|
|
52
|
+
"""Undo the most recent command.
|
|
53
|
+
|
|
54
|
+
Pops the command from the undo stack, executes its inverse,
|
|
55
|
+
and pushes the original command to the redo stack.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
file_manager: FileManager instance for audio I/O
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The command that was undone, or None if undo failed or stack is empty
|
|
62
|
+
"""
|
|
63
|
+
if not self._undo_stack:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
command = self._undo_stack.pop()
|
|
67
|
+
inverse_command = command.inverse()
|
|
68
|
+
|
|
69
|
+
if inverse_command.execute(file_manager):
|
|
70
|
+
self._redo_stack.append(command)
|
|
71
|
+
return command
|
|
72
|
+
|
|
73
|
+
self._undo_stack.append(command)
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def redo(self, file_manager: "FileManager") -> Optional[EditCommand]:
|
|
77
|
+
"""Redo the most recently undone command.
|
|
78
|
+
|
|
79
|
+
Pops the command from the redo stack, re-executes it,
|
|
80
|
+
and pushes it back to the undo stack.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
file_manager: FileManager instance for audio I/O
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The command that was redone, or None if redo failed or stack is empty
|
|
87
|
+
"""
|
|
88
|
+
if not self._redo_stack:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
command = self._redo_stack.pop()
|
|
92
|
+
|
|
93
|
+
if command.execute(file_manager):
|
|
94
|
+
self._undo_stack.append(command)
|
|
95
|
+
return command
|
|
96
|
+
|
|
97
|
+
self._redo_stack.append(command)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def can_undo(self) -> bool:
|
|
101
|
+
"""Check if there are commands that can be undone.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if undo is available, False otherwise
|
|
105
|
+
"""
|
|
106
|
+
return len(self._undo_stack) > 0
|
|
107
|
+
|
|
108
|
+
def can_redo(self) -> bool:
|
|
109
|
+
"""Check if there are commands that can be redone.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if redo is available, False otherwise
|
|
113
|
+
"""
|
|
114
|
+
return len(self._redo_stack) > 0
|
|
115
|
+
|
|
116
|
+
def clear(self) -> None:
|
|
117
|
+
"""Clear both undo and redo stacks.
|
|
118
|
+
|
|
119
|
+
Call this when switching to a different audio file.
|
|
120
|
+
"""
|
|
121
|
+
self._undo_stack.clear()
|
|
122
|
+
self._redo_stack.clear()
|
|
123
|
+
|
|
124
|
+
def peek_undo(self) -> Optional[EditCommand]:
|
|
125
|
+
"""Get the next command that would be undone without removing it.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The command at the top of the undo stack, or None if empty
|
|
129
|
+
"""
|
|
130
|
+
if self._undo_stack:
|
|
131
|
+
return self._undo_stack[-1]
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def peek_redo(self) -> Optional[EditCommand]:
|
|
135
|
+
"""Get the next command that would be redone without removing it.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The command at the top of the redo stack, or None if empty
|
|
139
|
+
"""
|
|
140
|
+
if self._redo_stack:
|
|
141
|
+
return self._redo_stack[-1]
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def undo_description(self) -> Optional[str]:
|
|
145
|
+
"""Get description of the next undo operation.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Description string or None if undo stack is empty
|
|
149
|
+
"""
|
|
150
|
+
cmd = self.peek_undo()
|
|
151
|
+
return cmd.description() if cmd else None
|
|
152
|
+
|
|
153
|
+
def redo_description(self) -> Optional[str]:
|
|
154
|
+
"""Get description of the next redo operation.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Description string or None if redo stack is empty
|
|
158
|
+
"""
|
|
159
|
+
cmd = self.peek_redo()
|
|
160
|
+
return cmd.description() if cmd else None
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def undo_count(self) -> int:
|
|
164
|
+
"""Get the number of commands in the undo stack."""
|
|
165
|
+
return len(self._undo_stack)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def redo_count(self) -> int:
|
|
169
|
+
"""Get the number of commands in the redo stack."""
|
|
170
|
+
return len(self._redo_stack)
|