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.
Files changed (140) hide show
  1. {revoxx-1.1.0/revoxx.egg-info → revoxx-1.1.1}/PKG-INFO +3 -2
  2. {revoxx-1.1.0 → revoxx-1.1.1}/pyproject.toml +3 -1
  3. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/constants.py +11 -2
  4. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/import_text_dialog.py +2 -1
  5. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/open_session_dialog.py +2 -1
  6. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/user_guide_dialog.py +114 -15
  7. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/utterance_list_base.py +2 -1
  8. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/selection_visualizer.py +50 -15
  9. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/playback_handler.py +11 -4
  10. revoxx-1.1.1/revoxx/ui/spectrogram/selection_interaction.py +408 -0
  11. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/widget.py +143 -415
  12. revoxx-1.1.1/revoxx/utils/adaptive_frame_rate.py +100 -0
  13. revoxx-1.1.1/revoxx/utils/tk_compat.py +43 -0
  14. {revoxx-1.1.0 → revoxx-1.1.1/revoxx.egg-info}/PKG-INFO +3 -2
  15. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx.egg-info/SOURCES.txt +3 -0
  16. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx.egg-info/requires.txt +3 -1
  17. {revoxx-1.1.0 → revoxx-1.1.1}/LICENSE +0 -0
  18. {revoxx-1.1.0 → revoxx-1.1.1}/MANIFEST.in +0 -0
  19. {revoxx-1.1.0 → revoxx-1.1.1}/README.md +0 -0
  20. {revoxx-1.1.0 → revoxx-1.1.1}/doc/import_raw_text.png +0 -0
  21. {revoxx-1.1.0 → revoxx-1.1.1}/doc/screenshot1.png +0 -0
  22. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/__init__.py +0 -0
  23. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/__main__.py +0 -0
  24. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/app.py +0 -0
  25. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/__init__.py +0 -0
  26. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/audio_buffer.py +0 -0
  27. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/audio_queue_processor.py +0 -0
  28. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/buffer_manager.py +0 -0
  29. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/editor.py +0 -0
  30. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/level_calculator.py +0 -0
  31. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/player.py +0 -0
  32. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/processors/__init__.py +0 -0
  33. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/processors/clipping_detector.py +0 -0
  34. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/processors/mel_spectrogram.py +0 -0
  35. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/processors/processor_base.py +0 -0
  36. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/queue_manager.py +0 -0
  37. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/recorder.py +0 -0
  38. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/shared_state.py +0 -0
  39. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/audio/worker_state.py +0 -0
  40. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/__init__.py +0 -0
  41. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/audio_controller.py +0 -0
  42. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/device_controller.py +0 -0
  43. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/dialog_controller.py +0 -0
  44. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/display_controller.py +0 -0
  45. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/edit_controller.py +0 -0
  46. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/file_operations_controller.py +0 -0
  47. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/navigation_controller.py +0 -0
  48. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/process_manager.py +0 -0
  49. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/controllers/session_controller.py +0 -0
  50. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/dataset/__init__.py +0 -0
  51. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/dataset/exporter.py +0 -0
  52. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/doc/USER_GUIDE.md +0 -0
  53. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/resources/keyboard_shortcuts.txt +0 -0
  54. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/resources/microphone.png +0 -0
  55. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/resources/templates/dataset_readme.txt +0 -0
  56. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/resources/templates/index_format_with_intensity.txt +0 -0
  57. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/resources/templates/index_format_without_intensity.txt +0 -0
  58. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/session/__init__.py +0 -0
  59. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/session/inspector.py +0 -0
  60. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/session/manager.py +0 -0
  61. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/session/models.py +0 -0
  62. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/session/script_parser.py +0 -0
  63. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/__init__.py +0 -0
  64. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/__init__.py +0 -0
  65. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/dataset_dialog.py +0 -0
  66. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/dialog_utils.py +0 -0
  67. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/find_dialog.py +0 -0
  68. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/help_dialog.py +0 -0
  69. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/new_session_dialog.py +0 -0
  70. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/progress_dialog.py +0 -0
  71. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/session_settings_dialog.py +0 -0
  72. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/dialogs/utterance_order_dialog.py +0 -0
  73. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/emotion_indicator.py +0 -0
  74. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/font_manager.py +0 -0
  75. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/frequency_axis.py +0 -0
  76. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/icon.py +0 -0
  77. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/info_overlay.py +0 -0
  78. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/level_meter/__init__.py +0 -0
  79. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/level_meter/config.py +0 -0
  80. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/level_meter/led_level_meter.py +0 -0
  81. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/menus/application_menu.py +0 -0
  82. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/menus/audio_devices.py +0 -0
  83. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/recording_display_state.py +0 -0
  84. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/__init__.py +0 -0
  85. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/__init__.py +0 -0
  86. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/clipping_visualizer.py +0 -0
  87. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/edge_indicator.py +0 -0
  88. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/playback_controller.py +0 -0
  89. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/controllers/zoom_controller.py +0 -0
  90. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/display_base.py +0 -0
  91. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/display_utils.py +0 -0
  92. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/mel_processor_manager.py +0 -0
  93. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/recording_display.py +0 -0
  94. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/recording_handler.py +0 -0
  95. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/selection_state.py +0 -0
  96. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/spectrogram/view_context.py +0 -0
  97. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/style_config.py +0 -0
  98. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/themes.py +0 -0
  99. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/widget_initializer.py +0 -0
  100. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/window_base.py +0 -0
  101. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/window_factory.py +0 -0
  102. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/ui/window_manager.py +0 -0
  103. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/__init__.py +0 -0
  104. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/active_recordings.py +0 -0
  105. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/audio_utils.py +0 -0
  106. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/config.py +0 -0
  107. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/device_manager.py +0 -0
  108. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/file_manager.py +0 -0
  109. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/process_cleanup.py +0 -0
  110. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/settings_manager.py +0 -0
  111. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/spectrogram_utils.py +0 -0
  112. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/state.py +0 -0
  113. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/text_importer.py +0 -0
  114. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx/utils/text_utils.py +0 -0
  115. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx.egg-info/dependency_links.txt +0 -0
  116. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx.egg-info/entry_points.txt +0 -0
  117. {revoxx-1.1.0 → revoxx-1.1.1}/revoxx.egg-info/top_level.txt +0 -0
  118. {revoxx-1.1.0 → revoxx-1.1.1}/scripts_module/__init__.py +0 -0
  119. {revoxx-1.1.0 → revoxx-1.1.1}/scripts_module/export.py +0 -0
  120. {revoxx-1.1.0 → revoxx-1.1.1}/scripts_module/vadiate.py +0 -0
  121. {revoxx-1.1.0 → revoxx-1.1.1}/setup.cfg +0 -0
  122. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_active_recordings.py +0 -0
  123. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_audio_controller.py +0 -0
  124. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_audio_queue_manager.py +0 -0
  125. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_config.py +0 -0
  126. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_dataset_exporter.py +0 -0
  127. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_device_controller.py +0 -0
  128. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_dialog_controller.py +0 -0
  129. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_display_controller.py +0 -0
  130. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_file_manager.py +0 -0
  131. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_file_operations_controller.py +0 -0
  132. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_ipc_communication.py +0 -0
  133. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_navigation_controller.py +0 -0
  134. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_new_session_dialog.py +0 -0
  135. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_process_manager.py +0 -0
  136. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_session_controller.py +0 -0
  137. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_session_manager.py +0 -0
  138. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_session_models.py +0 -0
  139. {revoxx-1.1.0 → revoxx-1.1.1}/tests/test_stable_sorting.py +0 -0
  140. {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.0
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
- 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"
@@ -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 = 10
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 = 20
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.trace("w", lambda *args: self._on_distribution_change())
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.trace("w", lambda *args: self._apply_filter())
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
- from tkinterweb import HtmlFrame
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
- self.text_widget = HtmlFrame(main_frame, messages_enabled=False)
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
- error_html = (
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 & convert markdown to HTML
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
- html_body = markdown2.markdown(
95
- content, extras=["tables", "fenced-code-blocks", "code-friendly"]
96
- )
97
-
98
- html_body = self._remove_links(html_body)
99
- html_content = self._wrap_html_with_css(html_body)
100
- self.text_widget.load_html(html_content)
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
- error_html = f"<html><body><p>{error_msg}</p></body></html>"
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.trace("w", self._on_search_changed)
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
- line.set_xdata([x_pos])
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((x_pos, -2))
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
- # Only show border lines if they are within visible range
257
- self._selection_start_line.set_xdata([x_start])
258
- start_visible = visible_start <= start_seconds <= visible_end
259
- self._selection_start_line.set_visible(start_visible)
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 = visible_start <= end_seconds <= visible_end
262
- self._selection_end_line.set_data([x_end, x_end], [0, ctx.n_mels - 8])
263
- self._selection_end_line.set_visible(end_visible)
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
- self._selection_start_text.set_position((x_start, -2))
276
- self._selection_start_text.set_text(_format_time(start_seconds))
277
- self._selection_start_text.set_visible(start_visible)
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
- self._selection_end_text.set_position((x_end, ctx.n_mels - 2))
281
- self._selection_end_text.set_text(_format_time(end_seconds))
282
- self._selection_end_text.set_visible(end_visible)
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
- # Check if still playing
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
- # Update interval
471
- update_interval = UIConstants.PLAYBACK_UPDATE_MS
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