revoxx 1.0.0.dev22__py3-none-any.whl → 1.0.1__py3-none-any.whl
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/__init__.py +9 -1
- revoxx/app.py +6 -0
- revoxx/controllers/display_controller.py +30 -0
- revoxx/controllers/navigation_controller.py +4 -25
- revoxx/controllers/process_manager.py +34 -0
- revoxx/controllers/session_controller.py +1 -4
- revoxx/dataset/exporter.py +121 -0
- revoxx/ui/dialogs/dataset_dialog.py +108 -5
- revoxx/ui/dialogs/open_session_dialog.py +54 -6
- revoxx/ui/dialogs/session_settings_dialog.py +262 -88
- revoxx/ui/dialogs/user_guide_dialog.py +260 -0
- revoxx/ui/dialogs/utterance_list_base.py +50 -13
- revoxx/ui/icon.py +1 -44
- revoxx/ui/menus/application_menu.py +13 -1
- revoxx/ui/window_base.py +8 -3
- revoxx/ui/window_factory.py +23 -30
- revoxx/utils/device_manager.py +1 -1
- revoxx/utils/process_cleanup.py +12 -4
- revoxx/utils/settings_manager.py +3 -0
- {revoxx-1.0.0.dev22.dist-info → revoxx-1.0.1.dist-info}/METADATA +65 -10
- {revoxx-1.0.0.dev22.dist-info → revoxx-1.0.1.dist-info}/RECORD +26 -25
- scripts_module/vadiate.py +19 -7
- {revoxx-1.0.0.dev22.dist-info → revoxx-1.0.1.dist-info}/WHEEL +0 -0
- {revoxx-1.0.0.dev22.dist-info → revoxx-1.0.1.dist-info}/entry_points.txt +0 -0
- {revoxx-1.0.0.dev22.dist-info → revoxx-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {revoxx-1.0.0.dev22.dist-info → revoxx-1.0.1.dist-info}/top_level.txt +0 -0
revoxx/__init__.py
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
"""Revoxx Recorder - A tool for recording emotional speech."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
try:
|
|
4
|
+
# Try to use versioningit for dynamic version detection
|
|
5
|
+
from versioningit import get_version
|
|
6
|
+
|
|
7
|
+
__version__ = get_version(root="../", config={})
|
|
8
|
+
except (ImportError, Exception):
|
|
9
|
+
# Fallback if versioningit is not installed or fails
|
|
10
|
+
__version__ = "1.0.0+dev"
|
|
11
|
+
|
|
4
12
|
__author__ = "Grammatek"
|
|
5
13
|
|
|
6
14
|
# Only import main entry point to avoid circular imports
|
revoxx/app.py
CHANGED
|
@@ -688,6 +688,12 @@ class Revoxx:
|
|
|
688
688
|
# Tkinter might have changed it during setup
|
|
689
689
|
self.cleanup_manager.refresh_sigint_handler()
|
|
690
690
|
|
|
691
|
+
# Show user guide dialog if configured
|
|
692
|
+
if self.settings_manager.get_setting("show_user_guide_at_startup", True):
|
|
693
|
+
from .ui.dialogs.user_guide_dialog import UserGuideDialog
|
|
694
|
+
|
|
695
|
+
UserGuideDialog(self.window.window, self.settings_manager)
|
|
696
|
+
|
|
691
697
|
self.window.focus_window()
|
|
692
698
|
self.window.window.mainloop()
|
|
693
699
|
|
|
@@ -220,6 +220,36 @@ class DisplayController:
|
|
|
220
220
|
"""Reset the level meter display."""
|
|
221
221
|
self.reset_level_meters()
|
|
222
222
|
|
|
223
|
+
def format_take_status(self, label: str) -> str:
|
|
224
|
+
"""Format the take status display string for a given label.
|
|
225
|
+
|
|
226
|
+
This returns current take information in the status bar.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
label: The utterance label (e.g., "utterance_001")
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
- Empty string if label is None or empty
|
|
233
|
+
- Just the label if no active_recordings exist
|
|
234
|
+
- Just the label if no takes exist for this utterance
|
|
235
|
+
- "label - Take X/Y" if takes exist, where X is the position of the
|
|
236
|
+
current take in the list and Y is the total number of takes
|
|
237
|
+
"""
|
|
238
|
+
if not label:
|
|
239
|
+
return ""
|
|
240
|
+
|
|
241
|
+
if not self.app.active_recordings:
|
|
242
|
+
return label
|
|
243
|
+
|
|
244
|
+
current_take = self.app.state.recording.get_current_take(label)
|
|
245
|
+
existing_takes = self.app.active_recordings.get_existing_takes(label)
|
|
246
|
+
|
|
247
|
+
if existing_takes and current_take in existing_takes:
|
|
248
|
+
position = existing_takes.index(current_take) + 1
|
|
249
|
+
return f"{label} - Take {position}/{len(existing_takes)}"
|
|
250
|
+
|
|
251
|
+
return label
|
|
252
|
+
|
|
223
253
|
def set_status(self, status: str, msg_type: MsgType = MsgType.TEMPORARY) -> None:
|
|
224
254
|
"""Set the status bar text.
|
|
225
255
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from ..constants import FileConstants
|
|
5
|
+
from ..constants import FileConstants, MsgType
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from ..app import Revoxx
|
|
@@ -134,10 +134,6 @@ class NavigationController:
|
|
|
134
134
|
# Update info overlay if visible
|
|
135
135
|
if self.app.window.info_panel_visible:
|
|
136
136
|
self.app.display_controller.update_info_panel()
|
|
137
|
-
else:
|
|
138
|
-
# No more takes in that direction
|
|
139
|
-
direction_text = "forward" if direction > 0 else "backward"
|
|
140
|
-
self.app.display_controller.set_status(f"No more takes {direction_text}")
|
|
141
137
|
|
|
142
138
|
def find_utterance(self, index: int) -> None:
|
|
143
139
|
"""Navigate directly to a specific utterance by index.
|
|
@@ -252,15 +248,8 @@ class NavigationController:
|
|
|
252
248
|
if not current_label:
|
|
253
249
|
return
|
|
254
250
|
|
|
255
|
-
current_take = self.app.state.recording.get_current_take(current_label)
|
|
256
|
-
if not self.app.active_recordings:
|
|
257
|
-
existing_takes = []
|
|
258
|
-
else:
|
|
259
|
-
existing_takes = self.app.active_recordings.get_existing_takes(
|
|
260
|
-
current_label
|
|
261
|
-
)
|
|
262
|
-
|
|
263
251
|
# Update label with filename if we have a recording
|
|
252
|
+
current_take = self.app.state.recording.get_current_take(current_label)
|
|
264
253
|
if current_take > 0:
|
|
265
254
|
filename = f"take_{current_take:03d}{FileConstants.AUDIO_FILE_EXTENSION}"
|
|
266
255
|
self.app.window.update_label_with_filename(current_label, filename)
|
|
@@ -277,18 +266,8 @@ class NavigationController:
|
|
|
277
266
|
if second:
|
|
278
267
|
second.update_label_with_filename(current_label)
|
|
279
268
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
position = existing_takes.index(current_take) + 1
|
|
283
|
-
total = len(existing_takes)
|
|
284
|
-
self.app.display_controller.set_status(
|
|
285
|
-
f"{current_label} - Take {position}/{total}"
|
|
286
|
-
)
|
|
287
|
-
elif not existing_takes:
|
|
288
|
-
# Show label even without recordings
|
|
289
|
-
self.app.display_controller.set_status(f"{current_label}")
|
|
290
|
-
else:
|
|
291
|
-
self.app.display_controller.set_status(f"{current_label}")
|
|
269
|
+
status_text = self.app.display_controller.format_take_status(current_label)
|
|
270
|
+
self.app.display_controller.set_status(status_text, MsgType.DEFAULT)
|
|
292
271
|
|
|
293
272
|
def after_recording_saved(self, label: str) -> None:
|
|
294
273
|
"""Called after a recording has been saved to disk.
|
|
@@ -77,6 +77,9 @@ class ProcessManager:
|
|
|
77
77
|
self.set_audio_queue_active(False)
|
|
78
78
|
self.set_save_path(None)
|
|
79
79
|
|
|
80
|
+
# Check for VAD availability
|
|
81
|
+
self._check_vad_availability()
|
|
82
|
+
|
|
80
83
|
def start_processes(self) -> None:
|
|
81
84
|
"""Start background recording and playback processes."""
|
|
82
85
|
if self.app.debug:
|
|
@@ -322,3 +325,34 @@ class ProcessManager:
|
|
|
322
325
|
and self.playback_process is not None
|
|
323
326
|
and self.playback_process.is_alive()
|
|
324
327
|
)
|
|
328
|
+
|
|
329
|
+
def _check_vad_availability(self) -> None:
|
|
330
|
+
"""Check if VAD support is available and store in manager_dict."""
|
|
331
|
+
try:
|
|
332
|
+
# Try to import the VAD module from scripts_module
|
|
333
|
+
from scripts_module import vadiate # noqa: F401
|
|
334
|
+
from silero_vad import load_silero_vad # noqa: F401
|
|
335
|
+
|
|
336
|
+
vad_available = True
|
|
337
|
+
if self.app.debug:
|
|
338
|
+
print("[ProcessManager] VAD support is available")
|
|
339
|
+
except ImportError:
|
|
340
|
+
vad_available = False
|
|
341
|
+
if self.app.debug:
|
|
342
|
+
print("[ProcessManager] VAD support is not available")
|
|
343
|
+
|
|
344
|
+
if self.manager_dict is not None:
|
|
345
|
+
self.manager_dict["vad_available"] = vad_available
|
|
346
|
+
|
|
347
|
+
def is_vad_available(self) -> bool:
|
|
348
|
+
"""Check if VAD support is available.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
True if VAD is available
|
|
352
|
+
"""
|
|
353
|
+
if self.manager_dict:
|
|
354
|
+
try:
|
|
355
|
+
return self.manager_dict.get("vad_available", False)
|
|
356
|
+
except (AttributeError, KeyError):
|
|
357
|
+
return False
|
|
358
|
+
return False
|
|
@@ -147,10 +147,7 @@ class SessionController:
|
|
|
147
147
|
self.reload_script_and_recordings()
|
|
148
148
|
|
|
149
149
|
# Then apply saved sort settings from session (after data is loaded)
|
|
150
|
-
|
|
151
|
-
self.app.active_recordings.set_sort(
|
|
152
|
-
session.sort_column, session.sort_reverse
|
|
153
|
-
)
|
|
150
|
+
self.app.active_recordings.set_sort(session.sort_column, session.sort_reverse)
|
|
154
151
|
|
|
155
152
|
self.app.window.window.title(f"Revoxx - {session.name}")
|
|
156
153
|
self.app.menu.update_recent_sessions()
|
revoxx/dataset/exporter.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Dataset exporter for converting Revoxx sessions to Talrómur 3 format."""
|
|
2
2
|
|
|
3
3
|
import shutil
|
|
4
|
+
import json
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import List, Dict, Tuple, Optional, Any
|
|
6
7
|
from collections import Counter
|
|
@@ -30,6 +31,7 @@ class DatasetExporter:
|
|
|
30
31
|
audio_format: str = "flac",
|
|
31
32
|
zero_intensity_emotions: List[str] = None,
|
|
32
33
|
include_intensity: bool = True,
|
|
34
|
+
include_vad: bool = False,
|
|
33
35
|
):
|
|
34
36
|
"""Initialize dataset exporter.
|
|
35
37
|
|
|
@@ -38,11 +40,13 @@ class DatasetExporter:
|
|
|
38
40
|
audio_format: Output audio format ('wav' or 'flac')
|
|
39
41
|
zero_intensity_emotions: List of emotions to set intensity to 0
|
|
40
42
|
include_intensity: Whether to include intensity column in index.tsv
|
|
43
|
+
include_vad: Whether to run VAD analysis on the exported dataset
|
|
41
44
|
"""
|
|
42
45
|
self.output_dir = Path(output_dir)
|
|
43
46
|
self.format = audio_format.lower()
|
|
44
47
|
self.zero_intensity_emotions = zero_intensity_emotions or ["neutral"]
|
|
45
48
|
self.include_intensity = include_intensity
|
|
49
|
+
self.include_vad = include_vad
|
|
46
50
|
|
|
47
51
|
def _group_sessions_by_speaker(self, session_paths: List[Path]) -> Dict:
|
|
48
52
|
"""Group sessions by speaker name.
|
|
@@ -172,6 +176,11 @@ class DatasetExporter:
|
|
|
172
176
|
}
|
|
173
177
|
)
|
|
174
178
|
|
|
179
|
+
# Run VAD processing if requested
|
|
180
|
+
if self.include_vad:
|
|
181
|
+
vad_stats = self._run_vad_processing(all_datasets, progress_callback)
|
|
182
|
+
total_statistics["vad_statistics"] = vad_stats
|
|
183
|
+
|
|
175
184
|
return all_datasets, total_statistics
|
|
176
185
|
|
|
177
186
|
def _process_emotion_group(
|
|
@@ -387,3 +396,115 @@ class DatasetExporter:
|
|
|
387
396
|
readme_path = dataset_dir / "README.txt"
|
|
388
397
|
with open(readme_path, "w", encoding="utf-8") as f:
|
|
389
398
|
f.write(readme_content)
|
|
399
|
+
|
|
400
|
+
def _run_vad_processing(
|
|
401
|
+
self, dataset_paths: List[Path], progress_callback=None
|
|
402
|
+
) -> Dict:
|
|
403
|
+
"""Run VAD processing on exported datasets using multiprocessing.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
dataset_paths: List of dataset directories to process
|
|
407
|
+
progress_callback: Optional progress callback (count, message)
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dictionary with total files processed and warnings
|
|
411
|
+
"""
|
|
412
|
+
try:
|
|
413
|
+
from scripts_module.vadiate import get_audio_files
|
|
414
|
+
import multiprocessing as mp
|
|
415
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
416
|
+
except ImportError:
|
|
417
|
+
return {} # VAD not available
|
|
418
|
+
|
|
419
|
+
# Count total files for progress
|
|
420
|
+
total_files = sum(len(get_audio_files(str(d))) for d in dataset_paths)
|
|
421
|
+
if total_files == 0:
|
|
422
|
+
return {}
|
|
423
|
+
|
|
424
|
+
processed = 0
|
|
425
|
+
vad_statistics = {"total_files": total_files, "warnings": []}
|
|
426
|
+
|
|
427
|
+
# Use process pool for parallel processing
|
|
428
|
+
# Each process handles VAD analysis for one complete dataset (speaker)
|
|
429
|
+
# This means if we export 3 speakers, we use up to 3 processes
|
|
430
|
+
# Each process analyzes all audio files within its assigned speaker's dataset
|
|
431
|
+
num_workers = min(mp.cpu_count(), len(dataset_paths))
|
|
432
|
+
|
|
433
|
+
with ProcessPoolExecutor(max_workers=num_workers) as executor:
|
|
434
|
+
# Submit one VAD processing task per dataset (per speaker)
|
|
435
|
+
# Each task processes all audio files in that speaker's dataset directory
|
|
436
|
+
future_to_dataset = {
|
|
437
|
+
executor.submit(self._process_dataset_vad, dataset_path): dataset_path
|
|
438
|
+
for dataset_path in dataset_paths
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# Process completed tasks
|
|
442
|
+
for future in as_completed(future_to_dataset):
|
|
443
|
+
dataset_path = future_to_dataset[future]
|
|
444
|
+
try:
|
|
445
|
+
result = future.result()
|
|
446
|
+
processed += result["files_processed"]
|
|
447
|
+
vad_statistics["warnings"].extend(result["warnings"])
|
|
448
|
+
if progress_callback:
|
|
449
|
+
progress_callback(
|
|
450
|
+
processed, f"VAD analysis: {processed}/{total_files}"
|
|
451
|
+
)
|
|
452
|
+
except Exception as e:
|
|
453
|
+
vad_statistics["warnings"].append(
|
|
454
|
+
f"VAD processing error for {dataset_path}: {e}"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return vad_statistics
|
|
458
|
+
|
|
459
|
+
@staticmethod
|
|
460
|
+
def _process_dataset_vad(dataset_path: Path) -> Dict:
|
|
461
|
+
"""Process VAD for a single dataset (one speaker's complete dataset).
|
|
462
|
+
|
|
463
|
+
This method runs in a separate process and handles all audio files
|
|
464
|
+
for one speaker. If multiple speakers were exported, each speaker's
|
|
465
|
+
dataset is processed by a different process in parallel.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
dataset_path: Path to the dataset directory for one speaker
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Dictionary with files processed and warnings
|
|
472
|
+
"""
|
|
473
|
+
from scripts_module.vadiate import (
|
|
474
|
+
get_audio_files,
|
|
475
|
+
process_audio,
|
|
476
|
+
load_silero_vad,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
vad_output = dataset_path / "vad.json"
|
|
480
|
+
audio_files = get_audio_files(str(dataset_path))
|
|
481
|
+
|
|
482
|
+
result_info = {"files_processed": 0, "warnings": []}
|
|
483
|
+
|
|
484
|
+
if not audio_files:
|
|
485
|
+
return result_info
|
|
486
|
+
|
|
487
|
+
# Load model for this process
|
|
488
|
+
model = load_silero_vad()
|
|
489
|
+
results = {}
|
|
490
|
+
|
|
491
|
+
for file_path in audio_files:
|
|
492
|
+
try:
|
|
493
|
+
rel_path, result, warnings = process_audio(
|
|
494
|
+
file_path,
|
|
495
|
+
model,
|
|
496
|
+
str(dataset_path),
|
|
497
|
+
use_dynamic_threshold=True,
|
|
498
|
+
collect_warnings=True,
|
|
499
|
+
)
|
|
500
|
+
results[rel_path] = result
|
|
501
|
+
result_info["warnings"].extend(warnings)
|
|
502
|
+
result_info["files_processed"] += 1
|
|
503
|
+
except Exception as e:
|
|
504
|
+
result_info["warnings"].append(f"VAD error for {file_path}: {e}")
|
|
505
|
+
|
|
506
|
+
# Save results
|
|
507
|
+
with open(vad_output, "w") as f:
|
|
508
|
+
json.dump(results, f, indent=2)
|
|
509
|
+
|
|
510
|
+
return result_info
|
|
@@ -41,16 +41,24 @@ class DatasetDialog:
|
|
|
41
41
|
# Entry field widths
|
|
42
42
|
ENTRY_WIDTH_STANDARD = 40
|
|
43
43
|
|
|
44
|
-
def __init__(
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
parent,
|
|
47
|
+
base_dir: Path,
|
|
48
|
+
settings_manager: SettingsManager,
|
|
49
|
+
process_manager=None,
|
|
50
|
+
):
|
|
45
51
|
"""Initialize dataset creation dialog.
|
|
46
52
|
|
|
47
53
|
Args:
|
|
48
54
|
parent: Parent window
|
|
49
55
|
base_dir: Base directory containing sessions
|
|
50
56
|
settings_manager: Shared SettingsManager instance
|
|
57
|
+
process_manager: Optional ProcessManager instance for VAD check
|
|
51
58
|
"""
|
|
52
59
|
self.parent = parent
|
|
53
60
|
self.settings_manager = settings_manager
|
|
61
|
+
self.process_manager = process_manager
|
|
54
62
|
self.result = None
|
|
55
63
|
|
|
56
64
|
# Use provided base_dir
|
|
@@ -229,11 +237,45 @@ class DatasetDialog:
|
|
|
229
237
|
self.settings_manager.settings, "export_include_intensity", True
|
|
230
238
|
)
|
|
231
239
|
)
|
|
240
|
+
options_frame = ttk.Frame(output_frame)
|
|
241
|
+
options_frame.grid(row=3, column=1, columnspan=2, sticky=tk.W, pady=2)
|
|
242
|
+
|
|
232
243
|
ttk.Checkbutton(
|
|
233
|
-
|
|
244
|
+
options_frame,
|
|
234
245
|
text="Include intensity levels in index.tsv",
|
|
235
246
|
variable=self.include_intensity_var,
|
|
236
|
-
).
|
|
247
|
+
).pack(anchor=tk.W)
|
|
248
|
+
|
|
249
|
+
# VAD support checkbox
|
|
250
|
+
self.include_vad_var = tk.BooleanVar(
|
|
251
|
+
value=getattr(self.settings_manager.settings, "export_include_vad", False)
|
|
252
|
+
)
|
|
253
|
+
self.vad_checkbox = ttk.Checkbutton(
|
|
254
|
+
options_frame,
|
|
255
|
+
text="Include VAD analysis",
|
|
256
|
+
variable=self.include_vad_var,
|
|
257
|
+
)
|
|
258
|
+
self.vad_checkbox.pack(anchor=tk.W, pady=(2, 0))
|
|
259
|
+
|
|
260
|
+
# Enable/disable VAD checkbox based on availability
|
|
261
|
+
vad_available = (
|
|
262
|
+
self.process_manager.is_vad_available() if self.process_manager else False
|
|
263
|
+
)
|
|
264
|
+
if vad_available:
|
|
265
|
+
self.vad_checkbox.configure(state="normal")
|
|
266
|
+
# Add tooltip
|
|
267
|
+
self._create_tooltip(
|
|
268
|
+
self.vad_checkbox,
|
|
269
|
+
"Voice Activity Detection provides speech segment timestamps",
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
self.vad_checkbox.configure(state="disabled")
|
|
273
|
+
self.include_vad_var.set(False)
|
|
274
|
+
# Add different tooltip for disabled state
|
|
275
|
+
self._create_tooltip(
|
|
276
|
+
self.vad_checkbox,
|
|
277
|
+
"VAD not available - install Revoxx with '[vad]' option to enable",
|
|
278
|
+
)
|
|
237
279
|
|
|
238
280
|
output_frame.columnconfigure(1, weight=1)
|
|
239
281
|
|
|
@@ -577,6 +619,9 @@ class DatasetDialog:
|
|
|
577
619
|
self.settings_manager.update_setting(
|
|
578
620
|
"export_include_intensity", self.include_intensity_var.get()
|
|
579
621
|
)
|
|
622
|
+
self.settings_manager.update_setting(
|
|
623
|
+
"export_include_vad", self.include_vad_var.get()
|
|
624
|
+
)
|
|
580
625
|
|
|
581
626
|
def _run_export(
|
|
582
627
|
self, session_paths: List[Path], output_dir: Path, dataset_name: Optional[str]
|
|
@@ -592,15 +637,24 @@ class DatasetDialog:
|
|
|
592
637
|
|
|
593
638
|
try:
|
|
594
639
|
# Create exporter
|
|
640
|
+
vad_enabled = self.include_vad_var.get() and (
|
|
641
|
+
self.process_manager.is_vad_available()
|
|
642
|
+
if self.process_manager
|
|
643
|
+
else False
|
|
644
|
+
)
|
|
595
645
|
exporter = DatasetExporter(
|
|
596
646
|
output_dir=output_dir,
|
|
597
647
|
audio_format=self.format_var.get(),
|
|
598
648
|
include_intensity=self.include_intensity_var.get(),
|
|
649
|
+
include_vad=vad_enabled,
|
|
599
650
|
)
|
|
600
651
|
|
|
601
652
|
# Export sessions
|
|
602
|
-
def progress_callback(count):
|
|
603
|
-
|
|
653
|
+
def progress_callback(count, message=None):
|
|
654
|
+
if message:
|
|
655
|
+
progress_dialog.update(count, message)
|
|
656
|
+
else:
|
|
657
|
+
progress_dialog.update(count, f"Processing utterance {count}")
|
|
604
658
|
|
|
605
659
|
dataset_paths, statistics = exporter.export_sessions(
|
|
606
660
|
session_paths,
|
|
@@ -684,6 +738,20 @@ class DatasetDialog:
|
|
|
684
738
|
summary += "\n" + "-" * 50 + "\n"
|
|
685
739
|
summary += f"⚠ Warning: {statistics['missing_recordings']} recordings were missing\n"
|
|
686
740
|
|
|
741
|
+
# Add VAD statistics if available
|
|
742
|
+
if "vad_statistics" in statistics and statistics["vad_statistics"]:
|
|
743
|
+
vad_stats = statistics["vad_statistics"]
|
|
744
|
+
summary += "\n" + "-" * 50 + "\n"
|
|
745
|
+
summary += (
|
|
746
|
+
f"VAD Analysis: {vad_stats.get('total_files', 0)} files processed\n"
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# Add warnings if any
|
|
750
|
+
if vad_stats.get("warnings"):
|
|
751
|
+
summary += "\nWarnings:\n"
|
|
752
|
+
for warning in vad_stats["warnings"]:
|
|
753
|
+
summary += f"{warning}\n"
|
|
754
|
+
|
|
687
755
|
# Insert text and make read-only
|
|
688
756
|
text_widget.insert("1.0", summary)
|
|
689
757
|
text_widget.configure(state="disabled")
|
|
@@ -714,6 +782,41 @@ class DatasetDialog:
|
|
|
714
782
|
"""Cancel dialog."""
|
|
715
783
|
self.dialog.destroy()
|
|
716
784
|
|
|
785
|
+
@staticmethod
|
|
786
|
+
def _create_tooltip(widget, text) -> None:
|
|
787
|
+
"""Create a tooltip for a widget.
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
widget: The widget to attach the tooltip to
|
|
791
|
+
text: The tooltip text
|
|
792
|
+
"""
|
|
793
|
+
tooltip = None
|
|
794
|
+
|
|
795
|
+
def on_enter(event):
|
|
796
|
+
nonlocal tooltip
|
|
797
|
+
tooltip = tk.Toplevel()
|
|
798
|
+
tooltip.wm_overrideredirect(True)
|
|
799
|
+
tooltip.wm_geometry(f"+{event.x_root+10}+{event.y_root+10}")
|
|
800
|
+
label = ttk.Label(
|
|
801
|
+
tooltip,
|
|
802
|
+
text=text,
|
|
803
|
+
justify=tk.LEFT,
|
|
804
|
+
background="#ffffe0",
|
|
805
|
+
relief=tk.SOLID,
|
|
806
|
+
borderwidth=1,
|
|
807
|
+
font=("TkDefaultFont", "9", "normal"),
|
|
808
|
+
)
|
|
809
|
+
label.pack()
|
|
810
|
+
|
|
811
|
+
def on_leave(event):
|
|
812
|
+
nonlocal tooltip
|
|
813
|
+
if tooltip:
|
|
814
|
+
tooltip.destroy()
|
|
815
|
+
tooltip = None
|
|
816
|
+
|
|
817
|
+
widget.bind("<Enter>", on_enter)
|
|
818
|
+
widget.bind("<Leave>", on_leave)
|
|
819
|
+
|
|
717
820
|
def show(self) -> Optional[Path]:
|
|
718
821
|
"""Show dialog and return result.
|
|
719
822
|
|
|
@@ -173,7 +173,9 @@ class OpenSessionDialog:
|
|
|
173
173
|
)
|
|
174
174
|
|
|
175
175
|
if new_dir:
|
|
176
|
-
|
|
176
|
+
new_path = Path(new_dir)
|
|
177
|
+
# Always load the directory, even if it's a .revoxx session
|
|
178
|
+
self._load_directory(new_path)
|
|
177
179
|
|
|
178
180
|
def _go_to_parent(self):
|
|
179
181
|
"""Navigate to parent directory."""
|
|
@@ -201,6 +203,27 @@ class OpenSessionDialog:
|
|
|
201
203
|
self.info_label.config(text="Directory does not exist")
|
|
202
204
|
return
|
|
203
205
|
|
|
206
|
+
# Check if current directory itself is a .revoxx session
|
|
207
|
+
if self._is_valid_session(self.current_dir):
|
|
208
|
+
session_info = self._get_session_info(self.current_dir)
|
|
209
|
+
if session_info:
|
|
210
|
+
# Show this directory as the only session
|
|
211
|
+
self.tree.insert(
|
|
212
|
+
"",
|
|
213
|
+
"end",
|
|
214
|
+
text=self.current_dir.name,
|
|
215
|
+
values=(
|
|
216
|
+
"Session",
|
|
217
|
+
session_info.get("speaker", ""),
|
|
218
|
+
session_info.get("emotion", ""),
|
|
219
|
+
session_info.get("utterances", ""),
|
|
220
|
+
session_info.get("recordings", ""),
|
|
221
|
+
),
|
|
222
|
+
tags=("session", "current"),
|
|
223
|
+
)
|
|
224
|
+
self.info_label.config(text="Current directory is a Revoxx session")
|
|
225
|
+
return
|
|
226
|
+
|
|
204
227
|
# Find sessions and subdirectories
|
|
205
228
|
sessions = []
|
|
206
229
|
subdirs = []
|
|
@@ -374,10 +397,31 @@ class OpenSessionDialog:
|
|
|
374
397
|
# Open the session
|
|
375
398
|
self._on_open()
|
|
376
399
|
|
|
400
|
+
def _is_valid_session(self, path: Path) -> bool:
|
|
401
|
+
"""Check if a path is a valid Revoxx session.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
path: Path to check
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
True if the path is a valid session, False otherwise
|
|
408
|
+
"""
|
|
409
|
+
return (
|
|
410
|
+
path.suffix == ".revoxx"
|
|
411
|
+
and path.is_dir()
|
|
412
|
+
and (path / "session.json").exists()
|
|
413
|
+
)
|
|
414
|
+
|
|
377
415
|
def _on_open(self):
|
|
378
416
|
"""Handle Open button click."""
|
|
379
417
|
selection = self.tree.selection()
|
|
418
|
+
|
|
419
|
+
# If no selection, check if current directory is a .revoxx session
|
|
380
420
|
if not selection:
|
|
421
|
+
if self._is_valid_session(self.current_dir):
|
|
422
|
+
self.result = self.current_dir
|
|
423
|
+
self.dialog.destroy()
|
|
424
|
+
return
|
|
381
425
|
messagebox.showwarning(
|
|
382
426
|
"No Selection", "Please select a session to open.", parent=self.dialog
|
|
383
427
|
)
|
|
@@ -385,6 +429,7 @@ class OpenSessionDialog:
|
|
|
385
429
|
|
|
386
430
|
item = self.tree.item(selection[0])
|
|
387
431
|
item_type = item["values"][0] if item["values"] else ""
|
|
432
|
+
item_tags = item.get("tags", [])
|
|
388
433
|
|
|
389
434
|
if item_type != "Session":
|
|
390
435
|
messagebox.showwarning(
|
|
@@ -394,12 +439,15 @@ class OpenSessionDialog:
|
|
|
394
439
|
)
|
|
395
440
|
return
|
|
396
441
|
|
|
397
|
-
#
|
|
398
|
-
|
|
399
|
-
|
|
442
|
+
# Determine the session path
|
|
443
|
+
if "current" in item_tags:
|
|
444
|
+
self.result = self.current_dir
|
|
445
|
+
else:
|
|
446
|
+
session_name = item["text"]
|
|
447
|
+
self.result = self.current_dir / session_name
|
|
400
448
|
|
|
401
|
-
#
|
|
402
|
-
if not self.
|
|
449
|
+
# Final validation
|
|
450
|
+
if not self._is_valid_session(self.result):
|
|
403
451
|
messagebox.showerror(
|
|
404
452
|
"Invalid Session",
|
|
405
453
|
"The selected directory is not a valid Revoxx session.",
|