revoxx 1.1.0__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.
- {revoxx-1.1.0/revoxx.egg-info → revoxx-1.1.1}/PKG-INFO +3 -2
- {revoxx-1.1.0 → revoxx-1.1.1}/pyproject.toml +3 -1
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/constants.py +11 -2
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/import_text_dialog.py +2 -1
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/open_session_dialog.py +2 -1
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/user_guide_dialog.py +114 -15
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/utterance_list_base.py +2 -1
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/selection_visualizer.py +50 -15
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/playback_handler.py +11 -4
- revoxx-1.1.1/revoxx/ui/spectrogram/selection_interaction.py +408 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/widget.py +143 -415
- revoxx-1.1.1/revoxx/utils/adaptive_frame_rate.py +100 -0
- revoxx-1.1.1/revoxx/utils/tk_compat.py +43 -0
- {revoxx-1.1.0 → revoxx-1.1.1/revoxx.egg-info}/PKG-INFO +3 -2
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx.egg-info/SOURCES.txt +3 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx.egg-info/requires.txt +3 -1
- {revoxx-1.1.0 → revoxx-1.1.1}/LICENSE +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/MANIFEST.in +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/README.md +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/doc/import_raw_text.png +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/doc/screenshot1.png +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/__main__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/app.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/audio_buffer.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/audio_queue_processor.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/buffer_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/editor.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/level_calculator.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/player.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/processors/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/processors/clipping_detector.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/processors/mel_spectrogram.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/processors/processor_base.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/queue_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/recorder.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/shared_state.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/worker_state.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/audio_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/device_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/dialog_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/display_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/edit_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/file_operations_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/navigation_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/process_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/session_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/dataset/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/dataset/exporter.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/doc/USER_GUIDE.md +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/resources/keyboard_shortcuts.txt +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/resources/microphone.png +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/resources/templates/dataset_readme.txt +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/resources/templates/index_format_with_intensity.txt +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/resources/templates/index_format_without_intensity.txt +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/session/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/session/inspector.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/session/manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/session/models.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/session/script_parser.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/dataset_dialog.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/dialog_utils.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/find_dialog.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/help_dialog.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/new_session_dialog.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/progress_dialog.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/session_settings_dialog.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/utterance_order_dialog.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/emotion_indicator.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/font_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/frequency_axis.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/icon.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/info_overlay.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/level_meter/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/level_meter/config.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/level_meter/led_level_meter.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/menus/application_menu.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/menus/audio_devices.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/recording_display_state.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/clipping_visualizer.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/edge_indicator.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/playback_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/zoom_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/display_base.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/display_utils.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/mel_processor_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/recording_display.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/recording_handler.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/selection_state.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/view_context.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/style_config.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/themes.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/widget_initializer.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/window_base.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/window_factory.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/window_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/active_recordings.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/audio_utils.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/config.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/device_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/file_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/process_cleanup.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/settings_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/spectrogram_utils.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/state.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/text_importer.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/text_utils.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx.egg-info/dependency_links.txt +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx.egg-info/entry_points.txt +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/revoxx.egg-info/top_level.txt +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/scripts_module/__init__.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/scripts_module/export.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/scripts_module/vadiate.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/setup.cfg +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_active_recordings.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_audio_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_audio_queue_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_config.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_dataset_exporter.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_device_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_dialog_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_display_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_file_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_file_operations_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_ipc_communication.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_navigation_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_new_session_dialog.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_process_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_session_controller.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_session_manager.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_session_models.py +0 -0
- {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_stable_sorting.py +0 -0
- {revoxx-1.1.0 → 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.1.
|
|
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,7 +32,8 @@ 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
|
-
|
|
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"
|
|
@@ -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",
|
|
@@ -162,7 +162,9 @@ class UIConstants:
|
|
|
162
162
|
# Playback display
|
|
163
163
|
PLAYBACK_LINE_WIDTH = 2
|
|
164
164
|
PLAYBACK_LINE_ALPHA = 0.8
|
|
165
|
-
PLAYBACK_UPDATE_MS =
|
|
165
|
+
PLAYBACK_UPDATE_MS = (
|
|
166
|
+
16 # Target ~60fps (adaptive frame rate adjusts for slower systems)
|
|
167
|
+
)
|
|
166
168
|
PLAYBACK_INITIAL_CHECK_MS = 50
|
|
167
169
|
PLAYBACK_IDLE_RETRY_MS = 20
|
|
168
170
|
PLAYBACK_WATCHDOG_NEAR_END_RATIO = 0.95
|
|
@@ -175,17 +177,24 @@ class UIConstants:
|
|
|
175
177
|
EDGE_INDICATOR_ALPHA = 0.8
|
|
176
178
|
EDGE_INDICATOR_TIMEOUT_MS = 350
|
|
177
179
|
|
|
180
|
+
# Tkinter event types
|
|
181
|
+
TK_EVENT_BUTTON_PRESS = "4"
|
|
182
|
+
TK_EVENT_SCROLL_UP = 4 # Linux mouse wheel scroll up
|
|
183
|
+
TK_EVENT_SCROLL_DOWN = 5 # Linux mouse wheel scroll down
|
|
184
|
+
|
|
178
185
|
# Selection and marker display
|
|
179
186
|
COLOR_POSITION_MARKER = "#00FFFF" # Cyan
|
|
180
187
|
COLOR_SELECTION_FILL = (1.0, 1.0, 1.0, 0.15) # Semi-transparent white (RGBA)
|
|
181
188
|
COLOR_SELECTION_BORDER = "#FFFFFF" # White
|
|
182
189
|
SELECTION_BORDER_WIDTH = 1
|
|
183
190
|
POSITION_MARKER_WIDTH = 2
|
|
191
|
+
SELECTION_LINE_OFFSET = 0.5 # spec_frames offset for edge visibility
|
|
192
|
+
POSITION_MARKER_OFFSET = 1.0 # spec_frames offset for thicker marker
|
|
184
193
|
SELECTION_DRAG_THRESHOLD = 5 # Pixels
|
|
185
194
|
MARKER_HOVER_THRESHOLD = 8 # Pixels - distance for resize cursor activation
|
|
186
195
|
|
|
187
196
|
# Timing (milliseconds)
|
|
188
|
-
ANIMATION_UPDATE_MS =
|
|
197
|
+
ANIMATION_UPDATE_MS = 100 # ~10fps (TkAgg + Tcl/Tk 9 needs ~100ms per frame on x86)
|
|
189
198
|
PLAYBACK_CHECK_MS = 50
|
|
190
199
|
PLAYBACK_STOP_DELAY_MS = 50 # Delay after stopping playback
|
|
191
200
|
FOCUS_DELAY_MS = 100
|
|
@@ -9,6 +9,7 @@ from typing import Optional, List
|
|
|
9
9
|
from matplotlib.figure import Figure
|
|
10
10
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
11
11
|
from ...utils.text_importer import TextImporter
|
|
12
|
+
from ...utils.tk_compat import trace_var_write
|
|
12
13
|
from .dialog_utils import setup_dialog_window
|
|
13
14
|
|
|
14
15
|
|
|
@@ -333,7 +334,7 @@ class ImportTextDialog:
|
|
|
333
334
|
validatecommand=vcmd_dist,
|
|
334
335
|
)
|
|
335
336
|
spinbox.pack(side=tk.LEFT, padx=(self.PADDING_SMALL, 0))
|
|
336
|
-
var
|
|
337
|
+
trace_var_write(var, lambda *args: self._on_distribution_change())
|
|
337
338
|
self.emotion_spinboxes.append(spinbox)
|
|
338
339
|
|
|
339
340
|
# Matplotlib plot
|
|
@@ -7,6 +7,7 @@ from typing import Optional
|
|
|
7
7
|
import json
|
|
8
8
|
|
|
9
9
|
from .dialog_utils import setup_dialog_window
|
|
10
|
+
from revoxx.utils.tk_compat import trace_var_write
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class OpenSessionDialog:
|
|
@@ -92,7 +93,7 @@ class OpenSessionDialog:
|
|
|
92
93
|
ttk.Label(filter_frame, text="Filter:").pack(side=tk.LEFT, padx=(0, 5))
|
|
93
94
|
|
|
94
95
|
self.filter_var = tk.StringVar()
|
|
95
|
-
self.filter_var
|
|
96
|
+
trace_var_write(self.filter_var, lambda *args: self._apply_filter())
|
|
96
97
|
filter_entry = ttk.Entry(filter_frame, textvariable=self.filter_var, width=30)
|
|
97
98
|
filter_entry.pack(side=tk.LEFT, padx=(0, 10))
|
|
98
99
|
|
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
import tkinter as tk
|
|
4
4
|
from tkinter import ttk
|
|
5
|
+
from tkinter.scrolledtext import ScrolledText
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
import re
|
|
7
8
|
import markdown2
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
# Try to import tkinterweb for HTML rendering, fall back to plain text if unavailable
|
|
11
|
+
# (tkinterweb requires Tkhtml which may not be available for Tcl/Tk 9)
|
|
12
|
+
try:
|
|
13
|
+
from tkinterweb import HtmlFrame
|
|
14
|
+
|
|
15
|
+
TKINTERWEB_AVAILABLE = True
|
|
16
|
+
except (ImportError, Exception):
|
|
17
|
+
TKINTERWEB_AVAILABLE = False
|
|
9
18
|
|
|
10
19
|
from .dialog_utils import setup_dialog_window
|
|
11
20
|
|
|
@@ -62,7 +71,26 @@ class UserGuideDialog:
|
|
|
62
71
|
)
|
|
63
72
|
self.checkbox.pack(side=tk.LEFT)
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
# Use HtmlFrame if available, otherwise fall back to ScrolledText
|
|
75
|
+
# Note: HtmlFrame import may succeed but instantiation can fail if Tkhtml
|
|
76
|
+
# binaries are not available (e.g., on Tcl/Tk 9 without compiled Tkhtml)
|
|
77
|
+
self.use_html = False
|
|
78
|
+
if TKINTERWEB_AVAILABLE:
|
|
79
|
+
try:
|
|
80
|
+
self.text_widget = HtmlFrame(main_frame, messages_enabled=False)
|
|
81
|
+
self.use_html = True
|
|
82
|
+
except Exception:
|
|
83
|
+
# Tkhtml not available - fall through to ScrolledText fallback
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
if not self.use_html:
|
|
87
|
+
self.text_widget = ScrolledText(
|
|
88
|
+
main_frame,
|
|
89
|
+
wrap=tk.WORD,
|
|
90
|
+
font=("Helvetica", 12),
|
|
91
|
+
padx=20,
|
|
92
|
+
pady=20,
|
|
93
|
+
)
|
|
66
94
|
self.text_widget.pack(fill=tk.BOTH, expand=True)
|
|
67
95
|
|
|
68
96
|
bottom_frame = ttk.Frame(main_frame)
|
|
@@ -80,29 +108,100 @@ class UserGuideDialog:
|
|
|
80
108
|
|
|
81
109
|
if not guide_path.exists():
|
|
82
110
|
error_msg = f"User Guide not found at: {guide_path}"
|
|
83
|
-
|
|
84
|
-
f"<html><body><p style='color: red;'>{error_msg}</p></body></html>"
|
|
85
|
-
)
|
|
86
|
-
self.text_widget.load_html(error_html)
|
|
111
|
+
self._display_error(error_msg)
|
|
87
112
|
raise FileNotFoundError(error_msg)
|
|
88
113
|
|
|
89
114
|
try:
|
|
90
|
-
# Read user guide content
|
|
115
|
+
# Read user guide content
|
|
91
116
|
with open(guide_path, "r", encoding="utf-8") as f:
|
|
92
117
|
content = f.read()
|
|
93
118
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
if self.use_html:
|
|
120
|
+
# Convert markdown to HTML for HtmlFrame
|
|
121
|
+
html_body = markdown2.markdown(
|
|
122
|
+
content, extras=["tables", "fenced-code-blocks", "code-friendly"]
|
|
123
|
+
)
|
|
124
|
+
html_body = self._remove_links(html_body)
|
|
125
|
+
html_content = self._wrap_html_with_css(html_body)
|
|
126
|
+
self.text_widget.load_html(html_content)
|
|
127
|
+
else:
|
|
128
|
+
# Display as formatted plain text in ScrolledText
|
|
129
|
+
self._display_markdown_as_text(content)
|
|
101
130
|
|
|
102
131
|
except Exception as e:
|
|
103
132
|
error_msg = f"Error loading guide: {e}"
|
|
104
|
-
|
|
133
|
+
self._display_error(error_msg)
|
|
134
|
+
|
|
135
|
+
def _display_error(self, error_msg: str):
|
|
136
|
+
"""Display an error message in the text widget."""
|
|
137
|
+
if self.use_html:
|
|
138
|
+
error_html = (
|
|
139
|
+
f"<html><body><p style='color: red;'>{error_msg}</p></body></html>"
|
|
140
|
+
)
|
|
105
141
|
self.text_widget.load_html(error_html)
|
|
142
|
+
else:
|
|
143
|
+
self.text_widget.insert(tk.END, f"Error: {error_msg}")
|
|
144
|
+
|
|
145
|
+
def _display_markdown_as_text(self, content: str):
|
|
146
|
+
"""Display markdown content as formatted plain text.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
content: Raw markdown content
|
|
150
|
+
"""
|
|
151
|
+
# Configure text tags for basic formatting
|
|
152
|
+
self.text_widget.tag_configure("h1", font=("Helvetica", 18, "bold"))
|
|
153
|
+
self.text_widget.tag_configure("h2", font=("Helvetica", 16, "bold"))
|
|
154
|
+
self.text_widget.tag_configure("h3", font=("Helvetica", 14, "bold"))
|
|
155
|
+
self.text_widget.tag_configure(
|
|
156
|
+
"code", font=("Courier", 11), background="#f0f0f0"
|
|
157
|
+
)
|
|
158
|
+
self.text_widget.tag_configure("bold", font=("Helvetica", 12, "bold"))
|
|
159
|
+
|
|
160
|
+
# Process markdown line by line
|
|
161
|
+
lines = content.split("\n")
|
|
162
|
+
for line in lines:
|
|
163
|
+
stripped = line.strip()
|
|
164
|
+
|
|
165
|
+
if stripped.startswith("### "):
|
|
166
|
+
self.text_widget.insert(tk.END, stripped[4:] + "\n", "h3")
|
|
167
|
+
elif stripped.startswith("## "):
|
|
168
|
+
self.text_widget.insert(tk.END, "\n" + stripped[3:] + "\n", "h2")
|
|
169
|
+
elif stripped.startswith("# "):
|
|
170
|
+
self.text_widget.insert(tk.END, stripped[2:] + "\n\n", "h1")
|
|
171
|
+
elif stripped.startswith("```"):
|
|
172
|
+
continue # Skip code fence markers
|
|
173
|
+
elif stripped.startswith("`") and stripped.endswith("`"):
|
|
174
|
+
self.text_widget.insert(tk.END, stripped[1:-1] + "\n", "code")
|
|
175
|
+
elif stripped.startswith("- ") or stripped.startswith("* "):
|
|
176
|
+
self.text_widget.insert(tk.END, " • " + stripped[2:] + "\n")
|
|
177
|
+
elif stripped.startswith("|"):
|
|
178
|
+
# Simple table handling - just show as text
|
|
179
|
+
self.text_widget.insert(tk.END, stripped + "\n", "code")
|
|
180
|
+
else:
|
|
181
|
+
# Handle inline formatting
|
|
182
|
+
self._insert_with_inline_formatting(line + "\n")
|
|
183
|
+
|
|
184
|
+
self.text_widget.config(state=tk.DISABLED)
|
|
185
|
+
|
|
186
|
+
def _insert_with_inline_formatting(self, text: str):
|
|
187
|
+
"""Insert text with basic inline markdown formatting.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
text: Text that may contain inline markdown
|
|
191
|
+
"""
|
|
192
|
+
# Simple approach: just insert plain text, stripping markdown markers
|
|
193
|
+
# Remove bold markers
|
|
194
|
+
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
|
|
195
|
+
text = re.sub(r"__([^_]+)__", r"\1", text)
|
|
196
|
+
# Remove italic markers
|
|
197
|
+
text = re.sub(r"\*([^*]+)\*", r"\1", text)
|
|
198
|
+
text = re.sub(r"_([^_]+)_", r"\1", text)
|
|
199
|
+
# Remove inline code markers
|
|
200
|
+
text = re.sub(r"`([^`]+)`", r"\1", text)
|
|
201
|
+
# Remove links, keep text
|
|
202
|
+
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
|
203
|
+
|
|
204
|
+
self.text_widget.insert(tk.END, text)
|
|
106
205
|
|
|
107
206
|
@staticmethod
|
|
108
207
|
def _remove_links(html: str) -> str:
|
|
@@ -7,6 +7,7 @@ import re
|
|
|
7
7
|
from enum import Enum
|
|
8
8
|
|
|
9
9
|
from .dialog_utils import setup_dialog_window
|
|
10
|
+
from revoxx.utils.tk_compat import trace_var_write
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class SortDirection(Enum):
|
|
@@ -324,7 +325,7 @@ class UtteranceListDialog:
|
|
|
324
325
|
)
|
|
325
326
|
|
|
326
327
|
self.search_var = tk.StringVar()
|
|
327
|
-
self.search_var
|
|
328
|
+
trace_var_write(self.search_var, self._on_search_changed)
|
|
328
329
|
self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var)
|
|
329
330
|
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
330
331
|
|
|
@@ -200,12 +200,23 @@ class SelectionVisualizer:
|
|
|
200
200
|
self._marker_time_text.set_visible(False)
|
|
201
201
|
return
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
# Offset only at absolute edges (time 0.0 or recording end)
|
|
204
|
+
offset = UIConstants.POSITION_MARKER_OFFSET
|
|
205
|
+
at_absolute_start = time_seconds <= 0
|
|
206
|
+
at_absolute_end = time_seconds >= ctx.recording_duration
|
|
207
|
+
if at_absolute_start:
|
|
208
|
+
x_pos_draw = x_pos + offset
|
|
209
|
+
elif at_absolute_end:
|
|
210
|
+
x_pos_draw = x_pos - offset
|
|
211
|
+
else:
|
|
212
|
+
x_pos_draw = x_pos
|
|
213
|
+
|
|
214
|
+
line.set_xdata([x_pos_draw])
|
|
204
215
|
line.set_visible(True)
|
|
205
216
|
|
|
206
217
|
# Update time text below the marker
|
|
207
218
|
if self._marker_time_text:
|
|
208
|
-
self._marker_time_text.set_position((
|
|
219
|
+
self._marker_time_text.set_position((x_pos_draw, -2))
|
|
209
220
|
self._marker_time_text.set_text(_format_time(time_seconds))
|
|
210
221
|
self._marker_time_text.set_visible(True)
|
|
211
222
|
|
|
@@ -253,14 +264,31 @@ class SelectionVisualizer:
|
|
|
253
264
|
self._selection_patch.set_height(ctx.n_mels)
|
|
254
265
|
self._selection_patch.set_visible(True)
|
|
255
266
|
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
267
|
+
# Show border lines only if they are within the visible range
|
|
268
|
+
# Apply offset only at absolute edges (time 0.0 or recording end)
|
|
269
|
+
offset = UIConstants.SELECTION_LINE_OFFSET
|
|
270
|
+
start_visible = start_seconds >= visible_start
|
|
271
|
+
end_visible = end_seconds <= visible_end
|
|
272
|
+
|
|
273
|
+
if start_visible:
|
|
274
|
+
# Offset only if at absolute start (time 0.0)
|
|
275
|
+
at_absolute_start = start_seconds <= 0
|
|
276
|
+
x_start_draw = x_start + offset if at_absolute_start else x_start
|
|
277
|
+
self._selection_start_line.set_xdata([x_start_draw])
|
|
278
|
+
self._selection_start_line.set_visible(True)
|
|
279
|
+
else:
|
|
280
|
+
self._selection_start_line.set_visible(False)
|
|
260
281
|
|
|
261
|
-
end_visible
|
|
262
|
-
|
|
263
|
-
|
|
282
|
+
if end_visible:
|
|
283
|
+
# Offset only if at absolute end (end of recording)
|
|
284
|
+
at_absolute_end = end_seconds >= ctx.recording_duration
|
|
285
|
+
x_end_draw = x_end - offset if at_absolute_end else x_end
|
|
286
|
+
self._selection_end_line.set_data(
|
|
287
|
+
[x_end_draw, x_end_draw], [0, ctx.n_mels - 8]
|
|
288
|
+
)
|
|
289
|
+
self._selection_end_line.set_visible(True)
|
|
290
|
+
else:
|
|
291
|
+
self._selection_end_line.set_visible(False)
|
|
264
292
|
|
|
265
293
|
# Show duration text in horizontal center, vertically near top
|
|
266
294
|
if self._selection_duration_text:
|
|
@@ -271,15 +299,22 @@ class SelectionVisualizer:
|
|
|
271
299
|
self._selection_duration_text.set_visible(True)
|
|
272
300
|
|
|
273
301
|
# Show time labels at selection boundaries (start at bottom, end at top)
|
|
302
|
+
# Only show labels if the corresponding marker is within visible range
|
|
274
303
|
if self._selection_start_text:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
304
|
+
if start_visible:
|
|
305
|
+
self._selection_start_text.set_position((x_start, -2))
|
|
306
|
+
self._selection_start_text.set_text(_format_time(start_seconds))
|
|
307
|
+
self._selection_start_text.set_visible(True)
|
|
308
|
+
else:
|
|
309
|
+
self._selection_start_text.set_visible(False)
|
|
278
310
|
|
|
279
311
|
if self._selection_end_text:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
312
|
+
if end_visible:
|
|
313
|
+
self._selection_end_text.set_position((x_end, ctx.n_mels - 2))
|
|
314
|
+
self._selection_end_text.set_text(_format_time(end_seconds))
|
|
315
|
+
self._selection_end_text.set_visible(True)
|
|
316
|
+
else:
|
|
317
|
+
self._selection_end_text.set_visible(False)
|
|
283
318
|
|
|
284
319
|
def _hide_selection(self) -> None:
|
|
285
320
|
"""Hide all selection visual elements."""
|
|
@@ -11,6 +11,7 @@ import time
|
|
|
11
11
|
from matplotlib.lines import Line2D
|
|
12
12
|
|
|
13
13
|
from ...constants import UIConstants
|
|
14
|
+
from ...utils.adaptive_frame_rate import get_adaptive_frame_rate, DEBUG_FPS
|
|
14
15
|
from .controllers import PlaybackController, ViewportMode, ZoomController
|
|
15
16
|
|
|
16
17
|
from ...audio.shared_state import (
|
|
@@ -337,7 +338,8 @@ class PlaybackHandler:
|
|
|
337
338
|
|
|
338
339
|
def _update_playback_position(self) -> None:
|
|
339
340
|
"""Update playback position from shared audio state."""
|
|
340
|
-
|
|
341
|
+
get_adaptive_frame_rate().frame_start()
|
|
342
|
+
|
|
341
343
|
if not self.shared_audio_state:
|
|
342
344
|
return
|
|
343
345
|
|
|
@@ -460,15 +462,20 @@ class PlaybackHandler:
|
|
|
460
462
|
self._schedule_next_frame()
|
|
461
463
|
|
|
462
464
|
def _schedule_next_frame(self) -> None:
|
|
463
|
-
"""Schedule next animation frame."""
|
|
465
|
+
"""Schedule next animation frame with adaptive timing."""
|
|
464
466
|
if self.animation_id:
|
|
465
467
|
try:
|
|
466
468
|
self.parent.after_cancel(self.animation_id)
|
|
467
469
|
except ValueError:
|
|
468
470
|
pass
|
|
469
471
|
|
|
470
|
-
|
|
471
|
-
update_interval =
|
|
472
|
+
afr = get_adaptive_frame_rate()
|
|
473
|
+
update_interval = afr.frame_end()
|
|
474
|
+
if DEBUG_FPS:
|
|
475
|
+
print(
|
|
476
|
+
f"[PLAY] overshoot={afr.get_overshoot():.1f}ms "
|
|
477
|
+
f"interval={update_interval}ms fps={afr.get_current_fps():.1f}"
|
|
478
|
+
)
|
|
472
479
|
|
|
473
480
|
self.animation_id = self.parent.after(
|
|
474
481
|
update_interval, self._update_playback_position
|