mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 05:40:26 +02:00
Performance improvements for voice mixer
This commit is contained in:
@@ -245,11 +245,12 @@ class VoiceMixer(QWidget):
|
|||||||
# Apply slider styling after widget is added to window (see showEvent)
|
# Apply slider styling after widget is added to window (see showEvent)
|
||||||
self._slider_style_applied = False
|
self._slider_style_applied = False
|
||||||
|
|
||||||
# Connect controls
|
# Connect controls with internal sync only (no external updates)
|
||||||
self.slider.valueChanged.connect(lambda val: self.spin_box.setValue(val / 100))
|
self.slider.valueChanged.connect(self._on_slider_changed)
|
||||||
self.spin_box.valueChanged.connect(
|
self.spin_box.valueChanged.connect(self._on_spinbox_changed)
|
||||||
lambda val: self.slider.setValue(int(val * 100))
|
|
||||||
)
|
# Flag to prevent recursive updates
|
||||||
|
self._syncing = False
|
||||||
|
|
||||||
# Layout for slider and labels
|
# Layout for slider and labels
|
||||||
slider_layout = QVBoxLayout()
|
slider_layout = QVBoxLayout()
|
||||||
@@ -320,6 +321,22 @@ class VoiceMixer(QWidget):
|
|||||||
self.spin_box.setEnabled(is_enabled)
|
self.spin_box.setEnabled(is_enabled)
|
||||||
self.slider.setEnabled(is_enabled)
|
self.slider.setEnabled(is_enabled)
|
||||||
|
|
||||||
|
def _on_slider_changed(self, val):
|
||||||
|
"""Handle slider value change - sync to spinbox without triggering external updates."""
|
||||||
|
if self._syncing:
|
||||||
|
return
|
||||||
|
self._syncing = True
|
||||||
|
self.spin_box.setValue(val / 100)
|
||||||
|
self._syncing = False
|
||||||
|
|
||||||
|
def _on_spinbox_changed(self, val):
|
||||||
|
"""Handle spinbox value change - sync to slider without triggering external updates."""
|
||||||
|
if self._syncing:
|
||||||
|
return
|
||||||
|
self._syncing = True
|
||||||
|
self.slider.setValue(int(val * 100))
|
||||||
|
self._syncing = False
|
||||||
|
|
||||||
def get_voice_weight(self):
|
def get_voice_weight(self):
|
||||||
if self.checkbox.isChecked():
|
if self.checkbox.isChecked():
|
||||||
return self.voice_name, self.spin_box.value()
|
return self.voice_name, self.spin_box.value()
|
||||||
@@ -403,6 +420,20 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
self._profile_dirty = {name: False for name in profiles}
|
self._profile_dirty = {name: False for name in profiles}
|
||||||
# Track unsaved states per profile
|
# Track unsaved states per profile
|
||||||
self._profile_states = {}
|
self._profile_states = {}
|
||||||
|
# Cache for loaded profiles to avoid repeated disk reads
|
||||||
|
self._cached_profiles = profiles.copy()
|
||||||
|
|
||||||
|
# Debounce timer for slider updates (prevents lag during rapid slider movement)
|
||||||
|
self._update_timer = QTimer(self)
|
||||||
|
self._update_timer.setSingleShot(True)
|
||||||
|
self._update_timer.setInterval(30) # 30ms debounce
|
||||||
|
self._update_timer.timeout.connect(self._do_debounced_update)
|
||||||
|
self._pending_weighted_update = False
|
||||||
|
self._pending_profile_modified = False
|
||||||
|
|
||||||
|
# Cache for voice weight labels to enable in-place updates
|
||||||
|
self._voice_labels = {} # voice_name -> HoverLabel widget
|
||||||
|
|
||||||
# Add subtitle_combo reference if parent has it
|
# Add subtitle_combo reference if parent has it
|
||||||
self.subtitle_combo = None
|
self.subtitle_combo = None
|
||||||
if parent is not None and hasattr(parent, "subtitle_combo"):
|
if parent is not None and hasattr(parent, "subtitle_combo"):
|
||||||
@@ -583,10 +614,8 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
self.btn_new_profile.clicked.connect(self.new_profile)
|
self.btn_new_profile.clicked.connect(self.new_profile)
|
||||||
self.btn_export_profiles.clicked.connect(self.export_all_profiles)
|
self.btn_export_profiles.clicked.connect(self.export_all_profiles)
|
||||||
self.btn_import_profiles.clicked.connect(self.import_profiles_dialog)
|
self.btn_import_profiles.clicked.connect(self.import_profiles_dialog)
|
||||||
# Detect modifications in voice mixers
|
# Note: Signal connections for voice mixers are already set up in add_voice()
|
||||||
for vm in self.voice_mixers:
|
# with debouncing for slider updates to prevent lag
|
||||||
vm.spin_box.valueChanged.connect(self.mark_profile_modified)
|
|
||||||
vm.checkbox.stateChanged.connect(lambda *_: self.mark_profile_modified())
|
|
||||||
|
|
||||||
# Update profile colors on initialization to show status
|
# Update profile colors on initialization to show status
|
||||||
self.update_profile_list_colors()
|
self.update_profile_list_colors()
|
||||||
@@ -772,9 +801,11 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
voice_mixer.checkbox.stateChanged.connect(
|
voice_mixer.checkbox.stateChanged.connect(
|
||||||
lambda state, vm=voice_mixer: self.handle_voice_checkbox(vm, state)
|
lambda state, vm=voice_mixer: self.handle_voice_checkbox(vm, state)
|
||||||
)
|
)
|
||||||
voice_mixer.spin_box.valueChanged.connect(self.update_weighted_sums)
|
# Use debounced updates for slider changes to prevent lag
|
||||||
|
voice_mixer.spin_box.valueChanged.connect(self._schedule_weighted_update)
|
||||||
|
voice_mixer.spin_box.valueChanged.connect(self._schedule_profile_modified)
|
||||||
|
# Checkbox changes are immediate since they're not high-frequency
|
||||||
voice_mixer.checkbox.stateChanged.connect(self.update_weighted_sums)
|
voice_mixer.checkbox.stateChanged.connect(self.update_weighted_sums)
|
||||||
voice_mixer.spin_box.valueChanged.connect(self.mark_profile_modified)
|
|
||||||
voice_mixer.checkbox.stateChanged.connect(
|
voice_mixer.checkbox.stateChanged.connect(
|
||||||
lambda *_: self.mark_profile_modified()
|
lambda *_: self.mark_profile_modified()
|
||||||
)
|
)
|
||||||
@@ -783,6 +814,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
def handle_voice_checkbox(self, voice_mixer, state):
|
def handle_voice_checkbox(self, voice_mixer, state):
|
||||||
if state == Qt.CheckState.Checked.value:
|
if state == Qt.CheckState.Checked.value:
|
||||||
self.last_enabled_voice = voice_mixer.voice_name
|
self.last_enabled_voice = voice_mixer.voice_name
|
||||||
|
# Checkbox changes are infrequent, so update immediately
|
||||||
self.update_weighted_sums()
|
self.update_weighted_sums()
|
||||||
|
|
||||||
def get_selected_voices(self):
|
def get_selected_voices(self):
|
||||||
@@ -792,13 +824,27 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
if v and v[1] > 0
|
if v and v[1] > 0
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_weighted_sums(self):
|
def _schedule_weighted_update(self):
|
||||||
# Clear previous labels
|
"""Schedule a debounced weighted sums update."""
|
||||||
while self.weighted_sums_layout.count():
|
self._pending_weighted_update = True
|
||||||
item = self.weighted_sums_layout.takeAt(0)
|
self._update_timer.start() # Restart the timer
|
||||||
if item and item.widget():
|
|
||||||
item.widget().deleteLater()
|
def _schedule_profile_modified(self):
|
||||||
|
"""Schedule a debounced profile modified update."""
|
||||||
|
self._pending_profile_modified = True
|
||||||
|
self._update_timer.start() # Restart the timer
|
||||||
|
|
||||||
|
def _do_debounced_update(self):
|
||||||
|
"""Execute pending debounced updates."""
|
||||||
|
if self._pending_weighted_update:
|
||||||
|
self._pending_weighted_update = False
|
||||||
|
self.update_weighted_sums()
|
||||||
|
if self._pending_profile_modified:
|
||||||
|
self._pending_profile_modified = False
|
||||||
|
self.mark_profile_modified()
|
||||||
|
|
||||||
|
def update_weighted_sums(self):
|
||||||
|
"""Update the voice weights display. Optimized for in-place updates during slider movement."""
|
||||||
# Get selected voices
|
# Get selected voices
|
||||||
selected = [
|
selected = [
|
||||||
(m.voice_name, m.spin_box.value())
|
(m.voice_name, m.spin_box.value())
|
||||||
@@ -823,22 +869,41 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
last = [(n, w) for n, w in selected if n == self.last_enabled_voice]
|
last = [(n, w) for n, w in selected if n == self.last_enabled_voice]
|
||||||
selected = others + last
|
selected = others + last
|
||||||
|
|
||||||
# Add voice labels
|
# Get current voice names in display
|
||||||
|
current_names = set(self._voice_labels.keys())
|
||||||
|
new_names = set(name for name, _ in selected)
|
||||||
|
|
||||||
|
# Remove labels for voices no longer selected
|
||||||
|
for name in current_names - new_names:
|
||||||
|
label = self._voice_labels.pop(name)
|
||||||
|
self.weighted_sums_layout.removeWidget(label)
|
||||||
|
label.deleteLater()
|
||||||
|
|
||||||
|
# Update or create labels
|
||||||
for name, weight in selected:
|
for name, weight in selected:
|
||||||
percentage = weight / total * 100
|
percentage = weight / total * 100
|
||||||
# Make the voice name bold and include percentage
|
label_text = f'<b><span style="color:{COLORS.get("BLUE")}">{name}: {percentage:.1f}%</span></b>'
|
||||||
voice_label = HoverLabel(
|
|
||||||
f'<b><span style="color:{COLORS.get("BLUE")}">{name}: {percentage:.1f}%</span></b>',
|
if name in self._voice_labels:
|
||||||
name,
|
# Update existing label in-place (fast path)
|
||||||
)
|
self._voice_labels[name].setText(label_text)
|
||||||
voice_label.setSizePolicy(
|
else:
|
||||||
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred
|
# Create new label only for newly added voices
|
||||||
)
|
voice_label = HoverLabel(label_text, name)
|
||||||
voice_label.delete_button.clicked.connect(
|
voice_label.setSizePolicy(
|
||||||
lambda _, vn=name: self.disable_voice_by_name(vn)
|
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred
|
||||||
)
|
)
|
||||||
self.weighted_sums_layout.addWidget(voice_label)
|
voice_label.delete_button.clicked.connect(
|
||||||
|
lambda _, vn=name: self.disable_voice_by_name(vn)
|
||||||
|
)
|
||||||
|
self._voice_labels[name] = voice_label
|
||||||
|
self.weighted_sums_layout.addWidget(voice_label)
|
||||||
else:
|
else:
|
||||||
|
# Clear all labels when no voices selected
|
||||||
|
for label in self._voice_labels.values():
|
||||||
|
self.weighted_sums_layout.removeWidget(label)
|
||||||
|
label.deleteLater()
|
||||||
|
self._voice_labels.clear()
|
||||||
self.error_label.show()
|
self.error_label.show()
|
||||||
self.weighted_sums_container.hide()
|
self.weighted_sums_container.hide()
|
||||||
|
|
||||||
@@ -871,6 +936,8 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
def load_profile_state(self, profile_name):
|
def load_profile_state(self, profile_name):
|
||||||
name = profile_name.lstrip("*")
|
name = profile_name.lstrip("*")
|
||||||
profiles = load_profiles()
|
profiles = load_profiles()
|
||||||
|
# Update cache when loading profiles
|
||||||
|
self._cached_profiles = profiles.copy()
|
||||||
# load voices and language from state or JSON
|
# load voices and language from state or JSON
|
||||||
if name in self._profile_states:
|
if name in self._profile_states:
|
||||||
state = self._profile_states[name]
|
state = self._profile_states[name]
|
||||||
@@ -905,6 +972,11 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
vm.slider.blockSignals(False)
|
vm.slider.blockSignals(False)
|
||||||
# sync enabled state
|
# sync enabled state
|
||||||
vm.toggle_inputs()
|
vm.toggle_inputs()
|
||||||
|
# Clear voice labels cache for clean update
|
||||||
|
for label in self._voice_labels.values():
|
||||||
|
self.weighted_sums_layout.removeWidget(label)
|
||||||
|
label.deleteLater()
|
||||||
|
self._voice_labels.clear()
|
||||||
self.update_weighted_sums()
|
self.update_weighted_sums()
|
||||||
|
|
||||||
def save_profile_by_name(self, name):
|
def save_profile_by_name(self, name):
|
||||||
@@ -918,6 +990,8 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
entry = {"voices": state, "language": self.language_combo.currentData()}
|
entry = {"voices": state, "language": self.language_combo.currentData()}
|
||||||
profiles[name] = entry
|
profiles[name] = entry
|
||||||
save_profiles(profiles)
|
save_profiles(profiles)
|
||||||
|
# Update cache to stay in sync
|
||||||
|
self._cached_profiles = profiles.copy()
|
||||||
self._profile_dirty[name] = False
|
self._profile_dirty[name] = False
|
||||||
del self._profile_states[name]
|
del self._profile_states[name]
|
||||||
self._virtual_new_profile = False
|
self._virtual_new_profile = False
|
||||||
@@ -1454,7 +1528,8 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
def update_profile_list_colors(self):
|
def update_profile_list_colors(self):
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
profiles = load_profiles()
|
# Use cached profiles to avoid disk reads during slider updates
|
||||||
|
profiles = self._cached_profiles
|
||||||
for i in range(self.profile_list.count()):
|
for i in range(self.profile_list.count()):
|
||||||
item = self.profile_list.item(i)
|
item = self.profile_list.item(i)
|
||||||
name = item.text().lstrip("*")
|
name = item.text().lstrip("*")
|
||||||
|
|||||||
Reference in New Issue
Block a user