mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Improvements for vıice_formula gui, reformat with black
This commit is contained in:
+1
-6
@@ -1,6 +1 @@
|
|||||||
- Enhanced EPUB handling by treating all items in chapter list (including anchors) as chapters, improving navigation and organization for poorly structured books, mentioned by @Darthagnon in #4
|
- Added voice mixing functionality that enables combining multiple voices into a single "Mixed voice", as suggested by @PulsarFTW in #1. (Special thanks to @jborza for making this feature possible with his contributions in #5)
|
||||||
- Fixed the issue with some chapters in EPUB files had missing content.
|
|
||||||
- Fixed the issue with some EPUB files only having one chapter caused the program to ignore the entire book.
|
|
||||||
- Fixed "utf-8' codec can't decode byte" error, mentioned by @nigelp in #3
|
|
||||||
- Added "Replace single newlines with spaces" option in the menu. This can be useful for texts that have imaginary line breaks.
|
|
||||||
- Improvements in code and documentation.
|
|
||||||
+49
-21
@@ -152,7 +152,9 @@ class HandlerDialog(QDialog):
|
|||||||
if item:
|
if item:
|
||||||
spine_docs.append(item.get_name()) # Use get_name() for href
|
spine_docs.append(item.get_name()) # Use get_name() for href
|
||||||
else:
|
else:
|
||||||
print(f"Warning: Spine item with id '{item_id}' not found in book items.")
|
print(
|
||||||
|
f"Warning: Spine item with id '{item_id}' not found in book items."
|
||||||
|
)
|
||||||
|
|
||||||
doc_order = {href: i for i, href in enumerate(spine_docs)}
|
doc_order = {href: i for i, href in enumerate(spine_docs)}
|
||||||
|
|
||||||
@@ -160,7 +162,7 @@ class HandlerDialog(QDialog):
|
|||||||
href = item.get_name()
|
href = item.get_name()
|
||||||
if href in doc_order: # Only process docs in spine
|
if href in doc_order: # Only process docs in spine
|
||||||
try:
|
try:
|
||||||
html_content = item.get_content().decode('utf-8', errors='ignore')
|
html_content = item.get_content().decode("utf-8", errors="ignore")
|
||||||
self.doc_content[href] = html_content
|
self.doc_content[href] = html_content
|
||||||
except Exception:
|
except Exception:
|
||||||
self.doc_content[href] = "" # Handle decoding errors
|
self.doc_content[href] = "" # Handle decoding errors
|
||||||
@@ -193,8 +195,10 @@ class HandlerDialog(QDialog):
|
|||||||
return -1 # Anchor not found by simple string search
|
return -1 # Anchor not found by simple string search
|
||||||
|
|
||||||
# Backtrack to the start of the tag '<'
|
# Backtrack to the start of the tag '<'
|
||||||
tag_start_pos = html_content.rfind('<', 0, pos)
|
tag_start_pos = html_content.rfind("<", 0, pos)
|
||||||
return tag_start_pos if tag_start_pos != -1 else 0 # Default to 0 if '<' not found
|
return (
|
||||||
|
tag_start_pos if tag_start_pos != -1 else 0
|
||||||
|
) # Default to 0 if '<' not found
|
||||||
|
|
||||||
def collect_toc_entries(entries):
|
def collect_toc_entries(entries):
|
||||||
collected = []
|
collected = []
|
||||||
@@ -213,23 +217,32 @@ class HandlerDialog(QDialog):
|
|||||||
title = section_or_link.title
|
title = section_or_link.title
|
||||||
href = getattr(section_or_link, "href", None)
|
href = getattr(section_or_link, "href", None)
|
||||||
elif isinstance(section_or_link, ebooklib.epub.Link):
|
elif isinstance(section_or_link, ebooklib.epub.Link):
|
||||||
href, title = section_or_link.href, section_or_link.title or section_or_link.href
|
href, title = (
|
||||||
|
section_or_link.href,
|
||||||
|
section_or_link.title or section_or_link.href,
|
||||||
|
)
|
||||||
|
|
||||||
if len(entry) > 1 and isinstance(entry[1], list):
|
if len(entry) > 1 and isinstance(entry[1], list):
|
||||||
children = entry[1]
|
children = entry[1]
|
||||||
|
|
||||||
if href:
|
if href:
|
||||||
base_href, fragment = href.split('#', 1) if '#' in href else (href, None)
|
base_href, fragment = (
|
||||||
if base_href in doc_order: # Only consider entries pointing to spine documents
|
href.split("#", 1) if "#" in href else (href, None)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
base_href in doc_order
|
||||||
|
): # Only consider entries pointing to spine documents
|
||||||
position = find_position(base_href, fragment)
|
position = find_position(base_href, fragment)
|
||||||
if position != -1: # Only add if position is valid
|
if position != -1: # Only add if position is valid
|
||||||
collected.append({
|
collected.append(
|
||||||
|
{
|
||||||
"href": href, # Use the original href from TOC as the key
|
"href": href, # Use the original href from TOC as the key
|
||||||
"title": title,
|
"title": title,
|
||||||
"doc_href": base_href,
|
"doc_href": base_href,
|
||||||
"position": position,
|
"position": position,
|
||||||
"doc_order": doc_order[base_href]
|
"doc_order": doc_order[base_href],
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if children:
|
if children:
|
||||||
collected.extend(collect_toc_entries(children))
|
collected.extend(collect_toc_entries(children))
|
||||||
@@ -247,7 +260,7 @@ class HandlerDialog(QDialog):
|
|||||||
all_content_html += self.doc_content.get(doc_href, "")
|
all_content_html += self.doc_content.get(doc_href, "")
|
||||||
|
|
||||||
if all_content_html:
|
if all_content_html:
|
||||||
soup = BeautifulSoup(all_content_html, 'html.parser')
|
soup = BeautifulSoup(all_content_html, "html.parser")
|
||||||
text = clean_text(soup.get_text()).strip()
|
text = clean_text(soup.get_text()).strip()
|
||||||
|
|
||||||
# Use the first spine document as the identifier
|
# Use the first spine document as the identifier
|
||||||
@@ -311,10 +324,10 @@ class HandlerDialog(QDialog):
|
|||||||
slice_html += self.doc_content.get(intermediate_doc_href, "")
|
slice_html += self.doc_content.get(intermediate_doc_href, "")
|
||||||
|
|
||||||
# 5. Extract text and store
|
# 5. Extract text and store
|
||||||
slice_soup = BeautifulSoup(slice_html, 'html.parser')
|
slice_soup = BeautifulSoup(slice_html, "html.parser")
|
||||||
|
|
||||||
# Remove sup and sub tags from the HTML before extracting text
|
# Remove sup and sub tags from the HTML before extracting text
|
||||||
for tag in slice_soup.find_all(['sup', 'sub']):
|
for tag in slice_soup.find_all(["sup", "sub"]):
|
||||||
tag.decompose()
|
tag.decompose()
|
||||||
|
|
||||||
text = clean_text(slice_soup.get_text()).strip()
|
text = clean_text(slice_soup.get_text()).strip()
|
||||||
@@ -337,9 +350,9 @@ class HandlerDialog(QDialog):
|
|||||||
prefix_html += first_doc_html[:first_pos]
|
prefix_html += first_doc_html[:first_pos]
|
||||||
|
|
||||||
if prefix_html.strip():
|
if prefix_html.strip():
|
||||||
prefix_soup = BeautifulSoup(prefix_html, 'html.parser')
|
prefix_soup = BeautifulSoup(prefix_html, "html.parser")
|
||||||
# Remove sup and sub tags
|
# Remove sup and sub tags
|
||||||
for tag in prefix_soup.find_all(['sup', 'sub']):
|
for tag in prefix_soup.find_all(["sup", "sub"]):
|
||||||
tag.decompose()
|
tag.decompose()
|
||||||
prefix_text = clean_text(prefix_soup.get_text()).strip()
|
prefix_text = clean_text(prefix_soup.get_text()).strip()
|
||||||
|
|
||||||
@@ -351,7 +364,9 @@ class HandlerDialog(QDialog):
|
|||||||
self.content_lengths[prefix_chapter_href] = len(prefix_text)
|
self.content_lengths[prefix_chapter_href] = len(prefix_text)
|
||||||
|
|
||||||
# Add a new entry to the TOC for the prefix content
|
# Add a new entry to the TOC for the prefix content
|
||||||
prefix_link = epub.Link(prefix_chapter_href, "Introduction", prefix_chapter_href)
|
prefix_link = epub.Link(
|
||||||
|
prefix_chapter_href, "Introduction", prefix_chapter_href
|
||||||
|
)
|
||||||
# Insert at beginning of TOC
|
# Insert at beginning of TOC
|
||||||
if isinstance(self.book.toc, list):
|
if isinstance(self.book.toc, list):
|
||||||
self.book.toc.insert(0, (prefix_link,))
|
self.book.toc.insert(0, (prefix_link,))
|
||||||
@@ -423,7 +438,11 @@ class HandlerDialog(QDialog):
|
|||||||
item.setData(0, Qt.UserRole, href)
|
item.setData(0, Qt.UserRole, href)
|
||||||
|
|
||||||
# Make item checkable if it has content
|
# Make item checkable if it has content
|
||||||
has_content = href and href in self.content_texts and self.content_texts[href].strip()
|
has_content = (
|
||||||
|
href
|
||||||
|
and href in self.content_texts
|
||||||
|
and self.content_texts[href].strip()
|
||||||
|
)
|
||||||
if has_content or children:
|
if has_content or children:
|
||||||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||||
is_checked = href and href in self.checked_chapters
|
is_checked = href and href in self.checked_chapters
|
||||||
@@ -652,9 +671,14 @@ class HandlerDialog(QDialog):
|
|||||||
self.previewEdit.setStyleSheet("QTextEdit { border: none; }")
|
self.previewEdit.setStyleSheet("QTextEdit { border: none; }")
|
||||||
|
|
||||||
# Create informative text label below preview
|
# Create informative text label below preview
|
||||||
self.previewInfoLabel = QLabel("*Note: You can modify the content later using the \"Edit\" button in the input box or by accessing the temporary files directory through settings.", self)
|
self.previewInfoLabel = QLabel(
|
||||||
|
'*Note: You can modify the content later using the "Edit" button in the input box or by accessing the temporary files directory through settings.',
|
||||||
|
self,
|
||||||
|
)
|
||||||
self.previewInfoLabel.setWordWrap(True)
|
self.previewInfoLabel.setWordWrap(True)
|
||||||
self.previewInfoLabel.setStyleSheet("QLabel { color: #666; font-style: italic; }")
|
self.previewInfoLabel.setStyleSheet(
|
||||||
|
"QLabel { color: #666; font-style: italic; }"
|
||||||
|
)
|
||||||
|
|
||||||
# Right panel layout (preview and info label)
|
# Right panel layout (preview and info label)
|
||||||
previewLayout = QVBoxLayout()
|
previewLayout = QVBoxLayout()
|
||||||
@@ -753,7 +777,9 @@ class HandlerDialog(QDialog):
|
|||||||
# Create splitter for left panel and preview
|
# Create splitter for left panel and preview
|
||||||
self.splitter = QSplitter(Qt.Horizontal)
|
self.splitter = QSplitter(Qt.Horizontal)
|
||||||
self.splitter.addWidget(leftWidget)
|
self.splitter.addWidget(leftWidget)
|
||||||
self.splitter.addWidget(rightWidget) # Now using rightWidget that includes preview and label
|
self.splitter.addWidget(
|
||||||
|
rightWidget
|
||||||
|
) # Now using rightWidget that includes preview and label
|
||||||
self.splitter.setSizes([280, 420])
|
self.splitter.setSizes([280, 420])
|
||||||
|
|
||||||
# Set main layout
|
# Set main layout
|
||||||
@@ -1031,7 +1057,9 @@ class HandlerDialog(QDialog):
|
|||||||
if text is None:
|
if text is None:
|
||||||
title = current.text(0)
|
title = current.text(0)
|
||||||
# Add title to preview even if no content
|
# Add title to preview even if no content
|
||||||
self.previewEdit.setPlainText(f"{title}\n\n(No content available for this item)")
|
self.previewEdit.setPlainText(
|
||||||
|
f"{title}\n\n(No content available for this item)"
|
||||||
|
)
|
||||||
elif not text.strip():
|
elif not text.strip():
|
||||||
title = current.text(0)
|
title = current.text(0)
|
||||||
self.previewEdit.setPlainText(f"{title}\n\n(This item is empty)")
|
self.previewEdit.setPlainText(f"{title}\n\n(This item is empty)")
|
||||||
|
|||||||
+12
-7
@@ -15,6 +15,7 @@ from voice_formulas import get_new_voice
|
|||||||
def get_sample_voice_text(lang_code):
|
def get_sample_voice_text(lang_code):
|
||||||
return SAMPLE_VOICE_TEXTS.get(lang_code, SAMPLE_VOICE_TEXTS["a"])
|
return SAMPLE_VOICE_TEXTS.get(lang_code, SAMPLE_VOICE_TEXTS["a"])
|
||||||
|
|
||||||
|
|
||||||
def detect_encoding(file_path):
|
def detect_encoding(file_path):
|
||||||
with open(file_path, "rb") as f:
|
with open(file_path, "rb") as f:
|
||||||
raw_data = f.read()
|
raw_data = f.read()
|
||||||
@@ -172,9 +173,7 @@ class ConversionThread(QThread):
|
|||||||
self.log_updated.emit(f"- Output format: {self.output_format}")
|
self.log_updated.emit(f"- Output format: {self.output_format}")
|
||||||
self.log_updated.emit(f"- Save option: {self.save_option}")
|
self.log_updated.emit(f"- Save option: {self.save_option}")
|
||||||
if self.replace_single_newlines:
|
if self.replace_single_newlines:
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(f"- Replace single newlines: Yes")
|
||||||
f"- Replace single newlines: Yes"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Display save_chapters_separately flag if it's set
|
# Display save_chapters_separately flag if it's set
|
||||||
if hasattr(self, "save_chapters_separately"):
|
if hasattr(self, "save_chapters_separately"):
|
||||||
@@ -206,7 +205,9 @@ class ConversionThread(QThread):
|
|||||||
text = self.file_name # Treat file_name as direct text input
|
text = self.file_name # Treat file_name as direct text input
|
||||||
else:
|
else:
|
||||||
encoding = detect_encoding(self.file_name)
|
encoding = detect_encoding(self.file_name)
|
||||||
with open(self.file_name, "r", encoding=encoding, errors="replace") as file:
|
with open(
|
||||||
|
self.file_name, "r", encoding=encoding, errors="replace"
|
||||||
|
) as file:
|
||||||
text = file.read()
|
text = file.read()
|
||||||
|
|
||||||
# Clean up text using utility function
|
# Clean up text using utility function
|
||||||
@@ -353,7 +354,7 @@ class ConversionThread(QThread):
|
|||||||
split_pattern = r"\n+"
|
split_pattern = r"\n+"
|
||||||
|
|
||||||
# Check if the voice is a formula and load it if necessary
|
# Check if the voice is a formula and load it if necessary
|
||||||
if '*' in self.voice:
|
if "*" in self.voice:
|
||||||
loaded_voice = get_new_voice(tts, self.voice, self.use_gpu)
|
loaded_voice = get_new_voice(tts, self.voice, self.use_gpu)
|
||||||
else:
|
else:
|
||||||
loaded_voice = self.voice
|
loaded_voice = self.voice
|
||||||
@@ -493,7 +494,9 @@ class ConversionThread(QThread):
|
|||||||
chapter_srt_path = os.path.join(
|
chapter_srt_path = os.path.join(
|
||||||
chapters_out_dir, f"{chapter_filename}.srt"
|
chapters_out_dir, f"{chapter_filename}.srt"
|
||||||
)
|
)
|
||||||
with open(chapter_srt_path, "w", encoding="utf-8", errors="replace") as srt_file:
|
with open(
|
||||||
|
chapter_srt_path, "w", encoding="utf-8", errors="replace"
|
||||||
|
) as srt_file:
|
||||||
for i, (start, end, text) in enumerate(
|
for i, (start, end, text) in enumerate(
|
||||||
chapter_subtitle_entries, 1
|
chapter_subtitle_entries, 1
|
||||||
):
|
):
|
||||||
@@ -534,7 +537,9 @@ class ConversionThread(QThread):
|
|||||||
srt_path = os.path.splitext(out_path)[0] + ".srt"
|
srt_path = os.path.splitext(out_path)[0] + ".srt"
|
||||||
sf.write(out_path, audio, 24000, format=self.output_format)
|
sf.write(out_path, audio, 24000, format=self.output_format)
|
||||||
if self.subtitle_mode != "Disabled":
|
if self.subtitle_mode != "Disabled":
|
||||||
with open(srt_path, "w", encoding="utf-8", errors="replace") as srt_file:
|
with open(
|
||||||
|
srt_path, "w", encoding="utf-8", errors="replace"
|
||||||
|
) as srt_file:
|
||||||
for i, (start, end, text) in enumerate(subtitle_entries, 1):
|
for i, (start, end, text) in enumerate(subtitle_entries, 1):
|
||||||
srt_file.write(
|
srt_file.write(
|
||||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
||||||
|
|||||||
+7
-3
@@ -1053,12 +1053,14 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
# if voice formula is not None, use the selected voice
|
# if voice formula is not None, use the selected voice
|
||||||
if self.mixed_voice_state:
|
if self.mixed_voice_state:
|
||||||
formula_components = [f"{weight} * {name}" for name, weight in self.mixed_voice_state]
|
formula_components = [
|
||||||
|
f"{weight} * {name}" for name, weight in self.mixed_voice_state
|
||||||
|
]
|
||||||
voice_formula = " + ".join(filter(None, formula_components))
|
voice_formula = " + ".join(filter(None, formula_components))
|
||||||
else:
|
else:
|
||||||
voice_formula = self.selected_voice
|
voice_formula = self.selected_voice
|
||||||
# selected language - use the first voice of the mix
|
# selected language - use the first voice of the mix
|
||||||
match = re.search(r'\b([a-z])', voice_formula)
|
match = re.search(r"\b([a-z])", voice_formula)
|
||||||
selected_lang = match.group(1)
|
selected_lang = match.group(1)
|
||||||
|
|
||||||
self.conversion_thread = ConversionThread(
|
self.conversion_thread = ConversionThread(
|
||||||
@@ -1083,7 +1085,9 @@ class abogen(QWidget):
|
|||||||
# Pass max_subtitle_words from config
|
# Pass max_subtitle_words from config
|
||||||
self.conversion_thread.max_subtitle_words = self.max_subtitle_words
|
self.conversion_thread.max_subtitle_words = self.max_subtitle_words
|
||||||
# Pass replace_single_newlines setting
|
# Pass replace_single_newlines setting
|
||||||
self.conversion_thread.replace_single_newlines = self.replace_single_newlines
|
self.conversion_thread.replace_single_newlines = (
|
||||||
|
self.replace_single_newlines
|
||||||
|
)
|
||||||
# Pass chapter count for EPUB or PDF files
|
# Pass chapter count for EPUB or PDF files
|
||||||
if self.selected_file_type in ["epub", "pdf"] and hasattr(
|
if self.selected_file_type in ["epub", "pdf"] and hasattr(
|
||||||
self, "selected_chapters"
|
self, "selected_chapters"
|
||||||
|
|||||||
+304
-125
@@ -10,251 +10,430 @@ from PyQt5.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QMessageBox
|
QMessageBox,
|
||||||
)
|
QFrame,
|
||||||
from PyQt5.QtCore import (
|
QLayout,
|
||||||
Qt,
|
QStyle,
|
||||||
QTimer
|
|
||||||
)
|
|
||||||
from constants import (
|
|
||||||
VOICES_INTERNAL,
|
|
||||||
FLAGS
|
|
||||||
)
|
)
|
||||||
|
from PyQt5.QtCore import Qt, QTimer, QPoint, QRect, QSize
|
||||||
|
from constants import VOICES_INTERNAL, FLAGS
|
||||||
|
|
||||||
# Constants for voice names and flags
|
# Constants
|
||||||
VOICE_MIXER_WIDTH = 160
|
VOICE_MIXER_WIDTH = 160
|
||||||
SLIDER_WIDTH = 32 # Added slider width
|
SLIDER_WIDTH = 32
|
||||||
MIN_WINDOW_WIDTH = 600 # Minimum window width
|
MIN_WINDOW_WIDTH = 600
|
||||||
MIN_WINDOW_HEIGHT = 400 # Minimum window height
|
MIN_WINDOW_HEIGHT = 400
|
||||||
INITIAL_WINDOW_WIDTH = 1000 # Initial window width
|
INITIAL_WINDOW_WIDTH = 1000
|
||||||
INITIAL_WINDOW_HEIGHT = 500 # Initial window height
|
INITIAL_WINDOW_HEIGHT = 500
|
||||||
FEMALE = "👩🦰"
|
FEMALE, MALE = "👩🦰", "👨"
|
||||||
MALE = "👨"
|
|
||||||
|
|
||||||
|
class FlowLayout(QLayout):
|
||||||
|
def __init__(self, parent=None, margin=0, spacing=-1):
|
||||||
|
super().__init__(parent)
|
||||||
|
if parent:
|
||||||
|
self.setContentsMargins(margin, margin, margin, margin)
|
||||||
|
self.setSpacing(spacing)
|
||||||
|
self._item_list = []
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
item = self.takeAt(0)
|
||||||
|
while item:
|
||||||
|
item = self.takeAt(0)
|
||||||
|
|
||||||
|
def addItem(self, item):
|
||||||
|
self._item_list.append(item)
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
return len(self._item_list)
|
||||||
|
|
||||||
|
def expandingDirections(self):
|
||||||
|
return Qt.Orientations(Qt.Orientation(0))
|
||||||
|
|
||||||
|
def hasHeightForWidth(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
return self.minimumSize()
|
||||||
|
|
||||||
|
def itemAt(self, index):
|
||||||
|
if 0 <= index < len(self._item_list):
|
||||||
|
return self._item_list[index]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def takeAt(self, index):
|
||||||
|
if 0 <= index < len(self._item_list):
|
||||||
|
return self._item_list.pop(index)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def heightForWidth(self, width):
|
||||||
|
return self._do_layout(QRect(0, 0, width, 0), True)
|
||||||
|
|
||||||
|
def setGeometry(self, rect):
|
||||||
|
super().setGeometry(rect)
|
||||||
|
self._do_layout(rect, False)
|
||||||
|
|
||||||
|
def minimumSize(self):
|
||||||
|
size = QSize()
|
||||||
|
for item in self._item_list:
|
||||||
|
size = size.expandedTo(item.minimumSize())
|
||||||
|
margin, _, _, _ = self.getContentsMargins()
|
||||||
|
size += QSize(2 * margin, 2 * margin)
|
||||||
|
return size
|
||||||
|
|
||||||
|
def _do_layout(self, rect, test_only):
|
||||||
|
x, y = rect.x(), rect.y()
|
||||||
|
line_height = 0
|
||||||
|
spacing = self.spacing()
|
||||||
|
|
||||||
|
for item in self._item_list:
|
||||||
|
style = self.parentWidget().style() if self.parentWidget() else QStyle()
|
||||||
|
layout_spacing_x = style.layoutSpacing(
|
||||||
|
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
|
||||||
|
)
|
||||||
|
layout_spacing_y = style.layoutSpacing(
|
||||||
|
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
|
||||||
|
)
|
||||||
|
space_x = spacing if spacing >= 0 else layout_spacing_x
|
||||||
|
space_y = spacing if spacing >= 0 else layout_spacing_y
|
||||||
|
|
||||||
|
next_x = x + item.sizeHint().width() + space_x
|
||||||
|
if next_x - space_x > rect.right() and line_height > 0:
|
||||||
|
x = rect.x()
|
||||||
|
y = y + line_height + space_y
|
||||||
|
next_x = x + item.sizeHint().width() + space_x
|
||||||
|
line_height = 0
|
||||||
|
|
||||||
|
if not test_only:
|
||||||
|
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
|
||||||
|
|
||||||
|
x = next_x
|
||||||
|
line_height = max(line_height, item.sizeHint().height())
|
||||||
|
|
||||||
|
return y + line_height - rect.y()
|
||||||
|
|
||||||
|
|
||||||
class VoiceMixer(QWidget):
|
class VoiceMixer(QWidget):
|
||||||
def __init__(self, voice_name, language_icon, initial_status=False, initial_weight=0.0):
|
def __init__(
|
||||||
|
self, voice_name, language_icon, initial_status=False, initial_weight=0.0
|
||||||
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.voice_name = voice_name
|
self.voice_name = voice_name
|
||||||
|
|
||||||
# Set fixed width for this widget
|
|
||||||
self.setFixedWidth(VOICE_MIXER_WIDTH)
|
|
||||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
|
|
||||||
# TODO Set CSS for rounded corners
|
# TODO Set CSS for rounded corners
|
||||||
# self.setObjectName("VoiceMixer")
|
# self.setObjectName("VoiceMixer")
|
||||||
# self.setStyleSheet(self.ROUNDED_CSS)
|
# self.setStyleSheet(self.ROUNDED_CSS)
|
||||||
|
|
||||||
# Main Layout
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# Checkbox at the top
|
# Checkbox
|
||||||
self.checkbox = QCheckBox()
|
self.checkbox = QCheckBox()
|
||||||
self.checkbox.setChecked(initial_status)
|
self.checkbox.setChecked(initial_status)
|
||||||
self.checkbox.stateChanged.connect(self.toggle_inputs)
|
self.checkbox.stateChanged.connect(self.toggle_inputs)
|
||||||
layout.addWidget(self.checkbox, alignment=Qt.AlignCenter)
|
layout.addWidget(self.checkbox, alignment=Qt.AlignCenter)
|
||||||
|
|
||||||
voice_gender = self.get_voice_gender()
|
# Voice name label
|
||||||
|
voice_gender = (
|
||||||
|
FEMALE
|
||||||
|
if self.voice_name in VOICES_INTERNAL and self.voice_name[1] == "f"
|
||||||
|
else MALE
|
||||||
|
)
|
||||||
name = voice_name[3:].capitalize()
|
name = voice_name[3:].capitalize()
|
||||||
name_label = QLabel(f"{language_icon} {voice_gender} {name}")
|
|
||||||
name_layout = QHBoxLayout()
|
name_layout = QHBoxLayout()
|
||||||
name_layout.addWidget(name_label)
|
name_layout.addWidget(
|
||||||
name_layout.setAlignment(name_label, Qt.AlignCenter)
|
QLabel(f"{language_icon} {voice_gender} {name}"), alignment=Qt.AlignCenter
|
||||||
|
)
|
||||||
layout.addLayout(name_layout)
|
layout.addLayout(name_layout)
|
||||||
|
|
||||||
# Input and Slider
|
# Spinbox and slider
|
||||||
self.spin_box = QDoubleSpinBox()
|
self.spin_box = QDoubleSpinBox()
|
||||||
self.spin_box.setRange(0, 1)
|
self.spin_box.setRange(0, 1)
|
||||||
self.spin_box.setSingleStep(0.01)
|
self.spin_box.setSingleStep(0.01)
|
||||||
self.spin_box.setDecimals(2)
|
self.spin_box.setDecimals(2)
|
||||||
self.spin_box.setValue(initial_weight)
|
self.spin_box.setValue(initial_weight)
|
||||||
|
|
||||||
self.slider = QSlider(Qt.Vertical) # Set slider orientation to vertical
|
self.slider = QSlider(Qt.Vertical)
|
||||||
self.slider.setRange(0, 100)
|
self.slider.setRange(0, 100)
|
||||||
self.slider.setValue(int(initial_weight * 100))
|
self.slider.setValue(int(initial_weight * 100))
|
||||||
self.slider.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Fixed width, expanding height
|
self.slider.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||||
self.slider.setFixedWidth(SLIDER_WIDTH) # Set fixed width for slider
|
self.slider.setFixedWidth(SLIDER_WIDTH)
|
||||||
self.slider.valueChanged.connect(
|
|
||||||
lambda val: self.spin_box.setValue(val / 100)
|
# Connect controls
|
||||||
)
|
self.slider.valueChanged.connect(lambda val: self.spin_box.setValue(val / 100))
|
||||||
self.spin_box.valueChanged.connect(
|
self.spin_box.valueChanged.connect(
|
||||||
lambda val: self.slider.setValue(int(val * 100))
|
lambda val: self.slider.setValue(int(val * 100))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Layout for slider and labels
|
||||||
slider_layout = QVBoxLayout()
|
slider_layout = QVBoxLayout()
|
||||||
slider_layout.addWidget(self.spin_box)
|
slider_layout.addWidget(self.spin_box)
|
||||||
slider_layout.addWidget(QLabel("1", alignment=Qt.AlignCenter))
|
slider_layout.addWidget(QLabel("1", alignment=Qt.AlignCenter))
|
||||||
slider_layout.addWidget(self.slider, alignment=Qt.AlignCenter, stretch=1) # Use stretch to expand slider
|
|
||||||
|
slider_center_layout = QHBoxLayout()
|
||||||
|
slider_center_layout.addWidget(self.slider, alignment=Qt.AlignHCenter)
|
||||||
|
slider_center_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
slider_center_widget = QWidget()
|
||||||
|
slider_center_widget.setLayout(slider_center_layout)
|
||||||
|
|
||||||
|
slider_layout.addWidget(slider_center_widget, stretch=1)
|
||||||
slider_layout.addWidget(QLabel("0", alignment=Qt.AlignCenter))
|
slider_layout.addWidget(QLabel("0", alignment=Qt.AlignCenter))
|
||||||
slider_layout.setStretch(2, 2) # Make slider take all available vertical space
|
slider_layout.setStretch(2, 1)
|
||||||
|
|
||||||
layout.addLayout(slider_layout, stretch=1) # Make slider layout expand
|
layout.addLayout(slider_layout, stretch=1)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
# Disable inputs initially if the checkbox is unchecked
|
|
||||||
self.toggle_inputs()
|
self.toggle_inputs()
|
||||||
|
|
||||||
def toggle_inputs(self):
|
def toggle_inputs(self):
|
||||||
"""Enable or disable inputs based on the checkbox state."""
|
|
||||||
is_enabled = self.checkbox.isChecked()
|
is_enabled = self.checkbox.isChecked()
|
||||||
self.spin_box.setEnabled(is_enabled)
|
self.spin_box.setEnabled(is_enabled)
|
||||||
self.slider.setEnabled(is_enabled)
|
self.slider.setEnabled(is_enabled)
|
||||||
|
|
||||||
def get_voice_gender(self):
|
|
||||||
if self.voice_name in VOICES_INTERNAL:
|
|
||||||
gender = self.voice_name[1]
|
|
||||||
return FEMALE if gender == "f" else MALE
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_formula_component(self):
|
|
||||||
if self.checkbox.isChecked():
|
|
||||||
weight = self.spin_box.value()
|
|
||||||
return f"{weight:.3f} * {self.voice_name.lower().replace(' ', '_')}"
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def get_voice_weight(self):
|
def get_voice_weight(self):
|
||||||
"""Return the voice and its weight if selected."""
|
|
||||||
if self.checkbox.isChecked():
|
if self.checkbox.isChecked():
|
||||||
return self.voice_name, self.spin_box.value()
|
return self.voice_name, self.spin_box.value()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class HoverLabel(QLabel):
|
||||||
|
def __init__(self, text, voice_name, parent=None):
|
||||||
|
super().__init__(text, parent)
|
||||||
|
self.voice_name = voice_name
|
||||||
|
self.setMouseTracking(True)
|
||||||
|
self.setStyleSheet(
|
||||||
|
"background-color: #e0e0e0; border-radius: 4px; padding: 3px 6px 3px 6px; margin: 2px;"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create delete button
|
||||||
|
self.delete_button = QPushButton("×", self)
|
||||||
|
self.delete_button.setFixedSize(16, 16)
|
||||||
|
self.delete_button.setStyleSheet(
|
||||||
|
"""
|
||||||
|
QPushButton {
|
||||||
|
background-color: red;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
border: none;
|
||||||
|
padding: 0px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #ff5555;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.delete_button.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.delete_button.hide()
|
||||||
|
|
||||||
|
def resizeEvent(self, event):
|
||||||
|
super().resizeEvent(event)
|
||||||
|
self.delete_button.move(self.width() - 16, 0)
|
||||||
|
|
||||||
|
def enterEvent(self, event):
|
||||||
|
self.delete_button.show()
|
||||||
|
|
||||||
|
def leaveEvent(self, event):
|
||||||
|
self.delete_button.hide()
|
||||||
|
|
||||||
|
|
||||||
class VoiceFormulaDialog(QDialog):
|
class VoiceFormulaDialog(QDialog):
|
||||||
def __init__(self, parent=None, initial_state=None):
|
def __init__(self, parent=None, initial_state=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.setWindowTitle("Voice Mixer")
|
self.setWindowTitle("Voice Mixer")
|
||||||
self.setMinimumWidth(MIN_WINDOW_WIDTH)
|
self.setMinimumSize(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
|
||||||
self.setMinimumHeight(MIN_WINDOW_HEIGHT)
|
|
||||||
self.resize(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT)
|
self.resize(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT)
|
||||||
self.voice_mixers = []
|
self.voice_mixers = []
|
||||||
|
self.last_enabled_voice = None
|
||||||
|
|
||||||
# Main Layout
|
# Main Layout
|
||||||
main_layout = QVBoxLayout()
|
main_layout = QVBoxLayout()
|
||||||
|
|
||||||
# Header Label
|
# Header
|
||||||
self.header_label = QLabel("Select Voices For the Mix and Adjust Weights")
|
self.header_label = QLabel(
|
||||||
|
"Adjust voice weights to create your preferred voice mix."
|
||||||
|
)
|
||||||
|
self.header_label.setStyleSheet("font-size: 13px;")
|
||||||
|
self.header_label.setWordWrap(True)
|
||||||
main_layout.addWidget(self.header_label)
|
main_layout.addWidget(self.header_label)
|
||||||
|
|
||||||
# Weighted Sums Label
|
# Error message
|
||||||
self.weighted_sums_label = QLabel()
|
self.error_label = QLabel(
|
||||||
self.weighted_sums_label.setAlignment(Qt.AlignCenter) # Center align the label
|
"No voices selected or all weights are 0. Please select at least one voice and set its weight above 0."
|
||||||
self.weighted_sums_label.setWordWrap(True) # Enable word wrap
|
)
|
||||||
main_layout.addWidget(self.weighted_sums_label)
|
self.error_label.setStyleSheet("color: red; font-weight: bold;")
|
||||||
|
self.error_label.setWordWrap(True)
|
||||||
|
self.error_label.hide()
|
||||||
|
main_layout.addWidget(self.error_label)
|
||||||
|
|
||||||
# Scroll Area for Voice Panels
|
# Voice weights display
|
||||||
|
self.weighted_sums_container = QWidget()
|
||||||
|
self.weighted_sums_layout = FlowLayout(self.weighted_sums_container)
|
||||||
|
self.weighted_sums_layout.setSpacing(5)
|
||||||
|
self.weighted_sums_layout.setContentsMargins(5, 5, 5, 5)
|
||||||
|
main_layout.addWidget(self.weighted_sums_container)
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
separator = QFrame()
|
||||||
|
separator.setFrameShadow(QFrame.Sunken)
|
||||||
|
main_layout.addWidget(separator)
|
||||||
|
|
||||||
|
# Voice list scroll area
|
||||||
self.scroll_area = QScrollArea()
|
self.scroll_area = QScrollArea()
|
||||||
self.scroll_area.setWidgetResizable(True)
|
self.scroll_area.setWidgetResizable(True)
|
||||||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||||
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||||
self.scroll_area.viewport().installEventFilter(self) # Install event filter for wheel events
|
self.scroll_area.viewport().installEventFilter(self)
|
||||||
|
|
||||||
self.voice_list_widget = QWidget()
|
self.voice_list_widget = QWidget()
|
||||||
self.voice_list_layout = QHBoxLayout()
|
self.voice_list_layout = QHBoxLayout()
|
||||||
self.voice_list_widget.setLayout(self.voice_list_layout)
|
self.voice_list_widget.setLayout(self.voice_list_layout)
|
||||||
self.voice_list_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self.voice_list_widget.setSizePolicy(
|
||||||
|
QSizePolicy.Expanding, QSizePolicy.Expanding
|
||||||
|
)
|
||||||
self.scroll_area.setWidget(self.voice_list_widget)
|
self.scroll_area.setWidget(self.voice_list_widget)
|
||||||
main_layout.addWidget(self.scroll_area)
|
main_layout.addWidget(self.scroll_area, stretch=1)
|
||||||
|
|
||||||
# Add buttons
|
# Buttons
|
||||||
button_layout = QHBoxLayout()
|
button_layout = QHBoxLayout()
|
||||||
|
clear_all_button = QPushButton("Clear all")
|
||||||
ok_button = QPushButton("OK")
|
ok_button = QPushButton("OK")
|
||||||
cancel_button = QPushButton("Cancel")
|
cancel_button = QPushButton("Cancel")
|
||||||
|
|
||||||
# Connect buttons to appropriate slots
|
# Set OK button as default
|
||||||
|
ok_button.setDefault(True)
|
||||||
|
ok_button.setFocus()
|
||||||
|
|
||||||
|
# Connect buttons
|
||||||
|
clear_all_button.clicked.connect(self.clear_all_voices)
|
||||||
ok_button.clicked.connect(self.accept)
|
ok_button.clicked.connect(self.accept)
|
||||||
cancel_button.clicked.connect(self.reject)
|
cancel_button.clicked.connect(self.reject)
|
||||||
|
|
||||||
button_layout.addStretch() # Push buttons to the right
|
button_layout.addStretch()
|
||||||
|
button_layout.addWidget(clear_all_button)
|
||||||
button_layout.addWidget(ok_button)
|
button_layout.addWidget(ok_button)
|
||||||
button_layout.addWidget(cancel_button)
|
button_layout.addWidget(cancel_button)
|
||||||
|
|
||||||
main_layout.addLayout(button_layout)
|
main_layout.addLayout(button_layout)
|
||||||
|
|
||||||
self.setLayout(main_layout)
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
self.add_voices(initial_state)
|
# Setup voices and display
|
||||||
|
self.add_voices(initial_state or [])
|
||||||
self.update_weighted_sums()
|
self.update_weighted_sums()
|
||||||
|
|
||||||
def add_voices(self, initial_state):
|
def add_voices(self, initial_state):
|
||||||
"""Add voice mixers to the dialog based on the initial state and scroll to first enabled one."""
|
|
||||||
first_enabled_voice = None
|
first_enabled_voice = None
|
||||||
|
|
||||||
for voice in VOICES_INTERNAL:
|
for voice in VOICES_INTERNAL:
|
||||||
flag = FLAGS.get(voice[0], "")
|
flag = FLAGS.get(voice[0], "")
|
||||||
matching_voice = next((item for item in initial_state if item[0] == voice), None)
|
matching_voice = next(
|
||||||
|
(item for item in initial_state if item[0] == voice), None
|
||||||
|
)
|
||||||
initial_status = matching_voice is not None
|
initial_status = matching_voice is not None
|
||||||
initial_weight = matching_voice[1] if matching_voice else 0.0
|
initial_weight = matching_voice[1] if matching_voice else 1.0
|
||||||
voice_mixer = self.add_voice(voice, flag, initial_status, initial_weight)
|
voice_mixer = self.add_voice(voice, flag, initial_status, initial_weight)
|
||||||
# remember the first enabled voice
|
|
||||||
if initial_status and first_enabled_voice is None:
|
if initial_status and first_enabled_voice is None:
|
||||||
first_enabled_voice = voice_mixer
|
first_enabled_voice = voice_mixer
|
||||||
|
|
||||||
if first_enabled_voice:
|
if first_enabled_voice:
|
||||||
self.scroll_to_voice(first_enabled_voice)
|
QTimer.singleShot(
|
||||||
|
0, lambda: self.scroll_area.ensureWidgetVisible(first_enabled_voice)
|
||||||
|
)
|
||||||
|
|
||||||
def add_voice(self, voice_name, language_icon, initial_status=False, initial_weight=1.0):
|
def add_voice(
|
||||||
voice_mixer = VoiceMixer(voice_name, language_icon, initial_status, initial_weight)
|
self, voice_name, language_icon, initial_status=False, initial_weight=1.0
|
||||||
|
):
|
||||||
|
voice_mixer = VoiceMixer(
|
||||||
|
voice_name, language_icon, initial_status, initial_weight
|
||||||
|
)
|
||||||
self.voice_mixers.append(voice_mixer)
|
self.voice_mixers.append(voice_mixer)
|
||||||
self.voice_list_layout.addWidget(voice_mixer)
|
self.voice_list_layout.addWidget(voice_mixer)
|
||||||
# Connect signals to update weighted sums
|
voice_mixer.checkbox.stateChanged.connect(
|
||||||
voice_mixer.checkbox.stateChanged.connect(self.update_weighted_sums)
|
lambda state, vm=voice_mixer: self.handle_voice_checkbox(vm, state)
|
||||||
|
)
|
||||||
voice_mixer.spin_box.valueChanged.connect(self.update_weighted_sums)
|
voice_mixer.spin_box.valueChanged.connect(self.update_weighted_sums)
|
||||||
return voice_mixer
|
return voice_mixer
|
||||||
|
|
||||||
def scroll_to_voice(self, voice_mixer):
|
def handle_voice_checkbox(self, voice_mixer, state):
|
||||||
"""Scroll the QScrollArea to ensure the given VoiceMixer is visible."""
|
if state == Qt.Checked:
|
||||||
QTimer.singleShot(0, lambda: self.scroll_area.ensureWidgetVisible(voice_mixer))
|
self.last_enabled_voice = voice_mixer.voice_name
|
||||||
|
self.update_weighted_sums()
|
||||||
|
|
||||||
def get_selected_voices(self):
|
def get_selected_voices(self):
|
||||||
"""Return the list of selected voices and their weights."""
|
return [
|
||||||
selected_voices = [
|
v
|
||||||
mixer.get_voice_weight() for mixer in self.voice_mixers
|
for v in (m.get_voice_weight() for m in self.voice_mixers)
|
||||||
|
if v and v[1] > 0
|
||||||
]
|
]
|
||||||
return [voice for voice in selected_voices if voice] # Filter out None
|
|
||||||
|
|
||||||
def update_weighted_sums(self):
|
def update_weighted_sums(self):
|
||||||
selected = [(m.voice_name, m.spin_box.value()) for m in self.voice_mixers if m.checkbox.isChecked()]
|
# Clear previous labels
|
||||||
|
while self.weighted_sums_layout.count():
|
||||||
|
item = self.weighted_sums_layout.takeAt(0)
|
||||||
|
if item and item.widget():
|
||||||
|
item.widget().deleteLater()
|
||||||
|
|
||||||
|
# Get selected voices
|
||||||
|
selected = [
|
||||||
|
(m.voice_name, m.spin_box.value())
|
||||||
|
for m in self.voice_mixers
|
||||||
|
if m.checkbox.isChecked() and m.spin_box.value() > 0
|
||||||
|
]
|
||||||
|
|
||||||
total = sum(w for _, w in selected)
|
total = sum(w for _, w in selected)
|
||||||
|
|
||||||
if total > 0:
|
if total > 0:
|
||||||
lines = [f"{name}: {weight/total*100:.1f}%" for name, weight in selected]
|
self.error_label.hide()
|
||||||
joined = " | ".join(lines)
|
self.weighted_sums_container.show()
|
||||||
|
|
||||||
|
# Reorder so last enabled voice is at the end
|
||||||
|
if self.last_enabled_voice and any(
|
||||||
|
name == self.last_enabled_voice for name, _ in selected
|
||||||
|
):
|
||||||
|
others = [(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
|
||||||
|
|
||||||
|
# Add voice labels
|
||||||
|
for name, weight in selected:
|
||||||
|
percentage = weight / total * 100
|
||||||
|
voice_label = HoverLabel(f"{name}: {percentage:.1f}%", name)
|
||||||
|
voice_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
||||||
|
voice_label.delete_button.clicked.connect(
|
||||||
|
lambda _, vn=name: self.disable_voice_by_name(vn)
|
||||||
|
)
|
||||||
|
self.weighted_sums_layout.addWidget(voice_label)
|
||||||
else:
|
else:
|
||||||
joined = ""
|
self.error_label.show()
|
||||||
self.weighted_sums_label.setText(joined) # Remove the prefix
|
self.weighted_sums_container.hide()
|
||||||
|
|
||||||
|
def disable_voice_by_name(self, voice_name):
|
||||||
|
for mixer in self.voice_mixers:
|
||||||
|
if mixer.voice_name == voice_name:
|
||||||
|
mixer.checkbox.setChecked(False)
|
||||||
|
break
|
||||||
|
|
||||||
|
def clear_all_voices(self):
|
||||||
|
for mixer in self.voice_mixers:
|
||||||
|
mixer.checkbox.setChecked(False)
|
||||||
|
|
||||||
def eventFilter(self, source, event):
|
def eventFilter(self, source, event):
|
||||||
"""Event filter to handle mouse wheel events for horizontal scrolling."""
|
if source is self.scroll_area.viewport() and event.type() == event.Wheel:
|
||||||
if (source is self.scroll_area.viewport() and event.type() == event.Wheel):
|
# Skip if over an enabled slider
|
||||||
|
if any(
|
||||||
# Check if the event is over a slider
|
mixer.slider.underMouse() and mixer.slider.isEnabled()
|
||||||
# Check if mouse is over an enabled slider
|
for mixer in self.voice_mixers
|
||||||
if any(mixer.slider.underMouse() and mixer.slider.isEnabled() for mixer in self.voice_mixers):
|
):
|
||||||
return False
|
return False
|
||||||
# Convert vertical wheel movement to horizontal scrolling
|
|
||||||
|
# Horizontal scrolling
|
||||||
horiz_bar = self.scroll_area.horizontalScrollBar()
|
horiz_bar = self.scroll_area.horizontalScrollBar()
|
||||||
if event.angleDelta().y() > 0:
|
delta = -120 if event.angleDelta().y() > 0 else 120
|
||||||
horiz_bar.setValue(horiz_bar.value() - 120) # Scroll left
|
horiz_bar.setValue(horiz_bar.value() + delta)
|
||||||
else:
|
|
||||||
horiz_bar.setValue(horiz_bar.value() + 120) # Scroll right
|
|
||||||
return True
|
return True
|
||||||
return super().eventFilter(source, event)
|
return super().eventFilter(source, event)
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
|
||||||
"""Handle resize events to adjust slider heights"""
|
|
||||||
super().resizeEvent(event)
|
|
||||||
|
|
||||||
# Calculate available height for sliders
|
|
||||||
header_height = self.header_label.height() + self.weighted_sums_label.height()
|
|
||||||
button_area_height = 50 # Approximate height for button area
|
|
||||||
available_height = self.height() - header_height - button_area_height - 220 # Add more margin for safety
|
|
||||||
|
|
||||||
# Set slider height (don't make them too small)
|
|
||||||
slider_height = max(available_height, 100)
|
|
||||||
|
|
||||||
# Update all sliders
|
|
||||||
for mixer in self.voice_mixers:
|
|
||||||
mixer.slider.setFixedHeight(slider_height) # Use fixed height instead of minimum height
|
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
selected_voices = self.get_selected_voices()
|
selected_voices = self.get_selected_voices()
|
||||||
total_weight = sum(weight for _, weight in selected_voices)
|
total_weight = sum(weight for _, weight in selected_voices)
|
||||||
@@ -262,7 +441,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"Invalid Weights",
|
"Invalid Weights",
|
||||||
"The total weight of selected voices cannot be zero. Please select at least one voice or adjust the weights."
|
"The total weight of selected voices cannot be zero. Please select at least one voice or adjust the weights.",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
super().accept()
|
super().accept()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
from constants import VOICES_INTERNAL
|
from constants import VOICES_INTERNAL
|
||||||
|
|
||||||
|
|
||||||
# Calls parsing and loads the voice to gpu or cpu
|
# Calls parsing and loads the voice to gpu or cpu
|
||||||
def get_new_voice(pipeline, formula, use_gpu):
|
def get_new_voice(pipeline, formula, use_gpu):
|
||||||
try:
|
try:
|
||||||
@@ -13,6 +14,7 @@ def get_new_voice(pipeline, formula, use_gpu):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Failed to create voice: {str(e)}")
|
raise ValueError(f"Failed to create voice: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
# Parse the formula and get the combined voice tensor
|
# Parse the formula and get the combined voice tensor
|
||||||
def parse_voice_formula(pipeline, formula):
|
def parse_voice_formula(pipeline, formula):
|
||||||
if not formula.strip():
|
if not formula.strip():
|
||||||
@@ -24,11 +26,11 @@ def parse_voice_formula(pipeline, formula):
|
|||||||
total_weight = calculate_sum_from_formula(formula)
|
total_weight = calculate_sum_from_formula(formula)
|
||||||
|
|
||||||
# Split the formula into terms
|
# Split the formula into terms
|
||||||
voices = formula.split('+')
|
voices = formula.split("+")
|
||||||
|
|
||||||
for term in voices:
|
for term in voices:
|
||||||
# Parse each term (format: "0.333 * voice_name")
|
# Parse each term (format: "0.333 * voice_name")
|
||||||
weight, voice_name = term.strip().split('*')
|
weight, voice_name = term.strip().split("*")
|
||||||
weight = float(weight.strip())
|
weight = float(weight.strip())
|
||||||
# normalize the weight
|
# normalize the weight
|
||||||
weight /= total_weight if total_weight > 0 else 1.0
|
weight /= total_weight if total_weight > 0 else 1.0
|
||||||
@@ -48,7 +50,8 @@ def parse_voice_formula(pipeline, formula):
|
|||||||
|
|
||||||
return weighted_sum
|
return weighted_sum
|
||||||
|
|
||||||
|
|
||||||
def calculate_sum_from_formula(formula):
|
def calculate_sum_from_formula(formula):
|
||||||
weights = re.findall(r'([\d.]+) \*', formula)
|
weights = re.findall(r"([\d.]+) \*", formula)
|
||||||
total_sum = sum(float(weight) for weight in weights)
|
total_sum = sum(float(weight) for weight in weights)
|
||||||
return total_sum
|
return total_sum
|
||||||
Reference in New Issue
Block a user