Change of anki template to get formating in anki to work
This commit is contained in:
parent
aa614391b8
commit
23b4228b58
4 changed files with 427 additions and 379 deletions
136
anki_exporter.py
136
anki_exporter.py
|
|
@ -1,107 +1,141 @@
|
|||
# anki_exporter.py
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
import shutil
|
||||
import asyncio
|
||||
import genanki
|
||||
import edge_tts
|
||||
import shutil
|
||||
import database
|
||||
|
||||
async def generate_edge_audio(text, voice, output_path, rate_modifier="+0%"):
|
||||
"""Asynchronously streams data packages via the Microsoft Edge API pipeline."""
|
||||
try:
|
||||
communicate = edge_tts.Communicate(text=text, voice=voice, rate=rate_modifier)
|
||||
await communicate.save(output_path)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Edge-TTS synthesis anomaly: {e}")
|
||||
return False
|
||||
|
||||
def compile_anki_package(records, output_path, deck_name):
|
||||
"""
|
||||
Compiles database records into an .apkg package using native macOS TTS.
|
||||
Uses 'Monica' for Spanish targets and the default premium system voice for English.
|
||||
Compiles database records into a bidirectional card payload package.
|
||||
Resolves voice models dynamically by gender selection parameters and applies
|
||||
global speed coefficient rates from the active configurations.
|
||||
"""
|
||||
# Create a unique random Model ID and Deck ID for genanki
|
||||
model_id = 1684329011
|
||||
deck_id = 1684329012
|
||||
# Incrementing both forces a completely clean slate for both layout and deck container
|
||||
model_id = 1684329014
|
||||
deck_id = 1684329014
|
||||
|
||||
# Global Configuration Pace Resolver Mapping
|
||||
settings = database.load_all_settings() or {}
|
||||
config_speed = settings.get("tts_playback_speed", "1.0")
|
||||
|
||||
# Transform numeric string floats (e.g., 1.2) into Edge-TTS percentage strings (e.g., +20%)
|
||||
try:
|
||||
pct = int((float(config_speed) - 1.0) * 100)
|
||||
rate_string = f"{'+' if pct >= 0 else ''}{pct}%"
|
||||
except Exception:
|
||||
rate_string = "+0%"
|
||||
|
||||
# Define the Anki Card Layout structure with audio fields
|
||||
anki_model = genanki.Model(
|
||||
model_id,
|
||||
'Spanish Voice Trainer Model',
|
||||
'Spanish Bidirectional Multi-Note HTML Model',
|
||||
fields=[
|
||||
{'name': 'EnglishText'},
|
||||
{'name': 'SpanishText'},
|
||||
{'name': 'Notes'},
|
||||
{'name': 'AnkiNotes'},
|
||||
{'name': 'EnglishAudio'},
|
||||
{'name': 'SpanishAudio'}
|
||||
],
|
||||
templates=[
|
||||
{
|
||||
'name': 'Card 1',
|
||||
'qfmt': '<div style="font-family: Arial; font-size: 20px; text-align: center; color: #34495E;">'
|
||||
'Translate to Spanish:<br><br><b>{{EnglishText}}</b></div><div style="display:none;">{{EnglishAudio}}</div>',
|
||||
'name': 'Card 1: English ➔ Spanish',
|
||||
'qfmt': '<div style="font-family: Arial; font-size: 13px; font-weight: bold; color: #BDC3C7; text-align: center; letter-spacing: 1px;">TRANSLATE TO SPANISH:</div><br>'
|
||||
'<div style="font-family: Arial; font-size: 20px; text-align: center; color: #34495E;"><b>{{EnglishText}}</b></div>'
|
||||
'<div style="display:none;">{{EnglishAudio}}</div>',
|
||||
'afmt': '{{FrontSide}}<hr id="answer">'
|
||||
'<div style="font-family: Arial; font-size: 28px; text-align: center; color: #2980B9; font-weight: bold;">'
|
||||
'{{SpanishText}}</div><br>'
|
||||
'<div style="font-family: Arial; font-size: 14px; text-align: center; color: #7F8C8D; font-style: italic;">'
|
||||
'{{Notes}}</div><br>'
|
||||
'<div style="font-family: Arial; font-size: 28px; text-align: center; color: #2980B9; font-weight: bold;">{{SpanishText}}</div><br>'
|
||||
'<div style="font-family: Arial; font-size: 14px; text-align: center; color: #34495E;">{{Notes}}</div>'
|
||||
'{{#AnkiNotes}}<div style="font-family: Arial; font-size: 13px; text-align: center; color: #8E44AD; border-top: 1px dashed #E5E7E9; padding-top: 6px; margin-top: 6px;"><b>Anki Meta:</b> {{AnkiNotes}}</div>{{/AnkiNotes}}<br>'
|
||||
'<div style="text-align: center;">{{SpanishAudio}}</div>',
|
||||
},
|
||||
{
|
||||
'name': 'Card 2: Spanish ➔ English',
|
||||
'qfmt': '<div style="font-family: Arial; font-size: 13px; font-weight: bold; color: #E67E22; text-align: center; letter-spacing: 1px;">TRANSLATE TO ENGLISH:</div><br>'
|
||||
'<div style="font-family: Arial; font-size: 26px; text-align: center; color: #2980B9; font-weight: bold;"><b>{{SpanishText}}</b></div>'
|
||||
'<div style="display:none;">{{SpanishAudio}}</div>',
|
||||
'afmt': '{{FrontSide}}<hr id="answer">'
|
||||
'<div style="font-family: Arial; font-size: 22px; text-align: center; color: #2C3E50; font-weight: 500;">{{EnglishText}}</div><br>'
|
||||
'<div style="font-family: Arial; font-size: 14px; text-align: center; color: #34495E;">{{Notes}}</div>'
|
||||
'{{#AnkiNotes}}<div style="font-family: Arial; font-size: 13px; text-align: center; color: #8E44AD; border-top: 1px dashed #E5E7E9; padding-top: 6px; margin-top: 6px;"><b>Anki Meta:</b> {{AnkiNotes}}</div>{{/AnkiNotes}}<br>'
|
||||
'<div style="text-align: center;">{{EnglishAudio}}</div>',
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
deck = genanki.Deck(deck_id, deck_name)
|
||||
media_files = []
|
||||
media_files_to_pack = []
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# Process all records inside a secure temporary directory workspace
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
for idx, record in enumerate(records):
|
||||
en_text = record["en_text"]
|
||||
es_text = record["es_text"]
|
||||
gender_flag = record.get("gender", "Female")
|
||||
|
||||
# Map native neural voice files matching gender settings
|
||||
spanish_voice = "es-ES-AlvaroNeural" if gender_flag == "Male" else "es-ES-ElviraNeural"
|
||||
english_voice = "en-US-EmmaNeural"
|
||||
|
||||
# Extract and parse tags for native Anki tag support (split comma-separated text)
|
||||
raw_tags = record.get("tags") or ""
|
||||
note_tags = [t.strip().replace(" ", "_") for t in raw_tags.split(",") if t.strip()]
|
||||
|
||||
# Build text for visual notes area, appending tags if they exist
|
||||
notes_parts = []
|
||||
# if record.get('source_context'):
|
||||
# notes_parts.append(f"Context: {record['source_context']}")
|
||||
# #if record.get('notes'):
|
||||
# #notes_parts.append(f"Notes: {record['notes']}")
|
||||
# if raw_tags:
|
||||
# notes_parts.append(f"Tags: {raw_tags}")
|
||||
# Dynamic HTML Notes Mapping Builder
|
||||
notes_html_parts = []
|
||||
if record.get('notes') and record['notes'].strip():
|
||||
notes_html_parts.append(f"<div style='margin-bottom: 4px;'>{record['notes'].strip()}</div>")
|
||||
if record.get('source_context') and record['source_context'].strip():
|
||||
notes_html_parts.append(f"<div style='font-size: 12px; color: #95A5A6; font-style: italic;'>Context: {record['source_context'].strip()}</div>")
|
||||
|
||||
notes_display_text = " | ".join(notes_parts)
|
||||
notes_html = "".join(notes_html_parts)
|
||||
anki_notes_html = f"<div>{record['anki_notes'].strip()}</div>" if record.get('anki_notes') else ""
|
||||
|
||||
# Generate unique filenames for the media assets
|
||||
en_audio_filename = f"en_audio_{idx}.mp3"
|
||||
es_audio_filename = f"es_audio_{idx}.mp3"
|
||||
# Standard Unique Media Filenames
|
||||
en_audio_filename = f"edge_en_{idx}_{model_id}.mp3"
|
||||
es_audio_filename = f"edge_es_{idx}_{model_id}.mp3"
|
||||
|
||||
en_audio_path = os.path.join(tmpdir, en_audio_filename)
|
||||
es_audio_path = os.path.join(tmpdir, es_audio_filename)
|
||||
|
||||
try:
|
||||
# 1. Render English Audio using native macOS text-to-speech engine
|
||||
subprocess.run(
|
||||
["say", "-o", en_audio_path, "--data-format=Iface", en_text],
|
||||
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
media_files.append(en_audio_path)
|
||||
# English Audio Synthesis
|
||||
if loop.run_until_complete(generate_edge_audio(en_text, english_voice, en_audio_path, rate_string)):
|
||||
media_files_to_pack.append(en_audio_path)
|
||||
en_audio_field = f"[sound:{en_audio_filename}]"
|
||||
except Exception:
|
||||
else:
|
||||
en_audio_field = ""
|
||||
|
||||
try:
|
||||
# 2. Render Spanish Audio explicitly targeting the Monica voice profile
|
||||
subprocess.run(
|
||||
["say", "-v", "Monica", "-o", es_audio_path, "--data-format=Iface", es_text],
|
||||
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
media_files.append(es_audio_path)
|
||||
# Spanish Audio Synthesis
|
||||
if loop.run_until_complete(generate_edge_audio(es_text, spanish_voice, es_audio_path, rate_string)):
|
||||
media_files_to_pack.append(es_audio_path)
|
||||
es_audio_field = f"[sound:{es_audio_filename}]"
|
||||
except Exception:
|
||||
else:
|
||||
es_audio_field = ""
|
||||
|
||||
# Build the card note stack, natively injecting the parsed Anki tags list
|
||||
note = genanki.Note(
|
||||
model=anki_model,
|
||||
fields=[en_text, es_text, notes_display_text, en_audio_field, es_audio_field],
|
||||
fields=[en_text, es_text, notes_html, anki_notes_html, en_audio_field, es_audio_field],
|
||||
tags=note_tags
|
||||
)
|
||||
deck.add_note(note)
|
||||
|
||||
# Build package collection mapping archive pipelines
|
||||
# Build Package while media assets are guaranteed contextually active inside tmpdir
|
||||
package = genanki.Package(deck)
|
||||
package.media_files = media_files
|
||||
package.media_files = media_files_to_pack
|
||||
package.write_to_file(output_path)
|
||||
69
database.py
69
database.py
|
|
@ -18,7 +18,7 @@ def ensure_database_populated():
|
|||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# 1. Simplified unified translations table
|
||||
# 1. Unified translations table with dedicated anki_notes and gender tracks
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS translations (
|
||||
translation_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
|
@ -26,7 +26,9 @@ def ensure_database_populated():
|
|||
en_text TEXT NOT NULL,
|
||||
source_context TEXT,
|
||||
tags TEXT,
|
||||
notes TEXT
|
||||
notes TEXT,
|
||||
anki_notes TEXT DEFAULT '',
|
||||
gender TEXT DEFAULT 'Female'
|
||||
);
|
||||
""")
|
||||
|
||||
|
|
@ -77,7 +79,7 @@ def save_setting_to_db(key, value):
|
|||
|
||||
def get_all_translations_explicit():
|
||||
"""
|
||||
Retrieves all 784+ records using completely explicit,
|
||||
Retrieves all records using completely explicit,
|
||||
table-qualified column declarations for the engines.
|
||||
"""
|
||||
conn = get_connection()
|
||||
|
|
@ -90,7 +92,9 @@ def get_all_translations_explicit():
|
|||
translations.en_text,
|
||||
translations.source_context,
|
||||
translations.tags,
|
||||
translations.notes
|
||||
translations.notes,
|
||||
translations.anki_notes,
|
||||
translations.gender
|
||||
FROM translations
|
||||
ORDER BY translations.translation_id ASC;
|
||||
""")
|
||||
|
|
@ -110,7 +114,9 @@ def get_translation_by_id(translation_id):
|
|||
translations.en_text,
|
||||
translations.source_context,
|
||||
translations.tags,
|
||||
translations.notes
|
||||
translations.notes,
|
||||
translations.anki_notes,
|
||||
translations.gender
|
||||
FROM translations
|
||||
WHERE translations.translation_id = ?;
|
||||
""", (translation_id,))
|
||||
|
|
@ -119,21 +125,34 @@ def get_translation_by_id(translation_id):
|
|||
finally:
|
||||
conn.close()
|
||||
|
||||
def update_translation_record(translation_id, es_text, en_text, source_context, tags, notes):
|
||||
"""Saves sandbox interface edits directly back down into the table."""
|
||||
def update_translation_record(translation_id, es_text, en_text, source_context, tags, notes, gender, anki_notes):
|
||||
"""Saves sandbox interface edits directly back down into the table using named arguments."""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# The SQL uses :key syntax instead of ?
|
||||
cursor.execute("""
|
||||
UPDATE translations
|
||||
SET
|
||||
es_text = ?,
|
||||
en_text = ?,
|
||||
source_context = ?,
|
||||
tags = ?,
|
||||
notes = ?
|
||||
WHERE translation_id = ?;
|
||||
""", (es_text, en_text, source_context, tags, notes, translation_id))
|
||||
es_text = :es,
|
||||
en_text = :en,
|
||||
source_context = :ctx,
|
||||
tags = :tags,
|
||||
notes = :notes,
|
||||
gender = :gender,
|
||||
anki_notes = :anki
|
||||
WHERE translation_id = :id;
|
||||
""", {
|
||||
# The order inside this dictionary does not matter at all!
|
||||
"id": translation_id,
|
||||
"es": es_text,
|
||||
"en": en_text,
|
||||
"ctx": source_context,
|
||||
"tags": tags,
|
||||
"notes": notes,
|
||||
"gender": gender,
|
||||
"anki": anki_notes
|
||||
})
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
|
@ -151,15 +170,25 @@ def delete_translation_record(translation_id):
|
|||
finally:
|
||||
conn.close()
|
||||
|
||||
def insert_translation_record(es_text, en_text, source_context, tags, notes):
|
||||
"""Inserts a completely fresh record into the translations table."""
|
||||
conn = get_connection()
|
||||
def insert_translation_record(es_text, en_text, source_context, tags, notes, gender, anki_notes):
|
||||
"""Inserts a new record using named arguments so positional order doesn't matter."""
|
||||
conn = get_connection() # Corrected from get_db_connection
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO translations (es_text, en_text, source_context, tags, notes)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
""", (es_text, en_text, source_context, tags, notes))
|
||||
INSERT INTO translations (es_text, en_text, source_context, tags, notes, gender, anki_notes)
|
||||
VALUES (:es, :en, :ctx, :tags, :notes, :gender, :anki);
|
||||
""", {
|
||||
# SQLite maps these keys directly to the tokens above by name
|
||||
"en": en_text,
|
||||
"es": es_text,
|
||||
"ctx": source_context,
|
||||
"tags": tags,
|
||||
"notes": notes,
|
||||
"anki": anki_notes,
|
||||
"gender": gender
|
||||
})
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,24 +1,58 @@
|
|||
# tabs/sandbox_tab.py
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import threading
|
||||
import asyncio
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
|
||||
QHeaderView, QMessageBox, QFormLayout
|
||||
QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, \
|
||||
QHeaderView, QMessageBox, QFormLayout, QDialog, QTextEdit, QRadioButton, QButtonGroup
|
||||
)
|
||||
from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt
|
||||
import edge_tts
|
||||
import database
|
||||
|
||||
class TextEditorDialog(QDialog):
|
||||
"""A pop-up modal containing a large text field workspace for copy-pasting extra text blocks."""
|
||||
def __init__(self, title, initial_text="", parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(title)
|
||||
self.resize(500, 350)
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.editor = QTextEdit()
|
||||
self.editor.setPlainText(initial_text)
|
||||
self.editor.setStyleSheet("font-family: Arial; font-size: 14px; padding: 5px;")
|
||||
layout.addWidget(self.editor)
|
||||
|
||||
btn_layout = QHBoxLayout()
|
||||
self.btn_save = QPushButton("Save / Apply")
|
||||
self.btn_save.clicked.connect(self.accept)
|
||||
self.btn_cancel = QPushButton("Cancel")
|
||||
self.btn_cancel.clicked.connect(self.reject)
|
||||
|
||||
btn_layout.addStretch()
|
||||
btn_layout.addWidget(self.btn_cancel)
|
||||
btn_layout.addWidget(self.btn_save)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def get_text(self):
|
||||
return self.editor.toPlainText().strip()
|
||||
|
||||
|
||||
class SandboxTab(QWidget):
|
||||
# Signal emitted whenever data is added, modified, or deleted
|
||||
data_mutated = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Internal tracking variable to distinguish edits vs new entries
|
||||
self.selected_translation_id = None
|
||||
|
||||
# Main layout structure
|
||||
# Local item memory caching for instant search lookups
|
||||
self.cached_records = []
|
||||
self.current_notes_content = ""
|
||||
self.current_anki_notes_content = ""
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(30, 20, 30, 20)
|
||||
main_layout.setSpacing(15)
|
||||
|
|
@ -29,361 +63,312 @@ class SandboxTab(QWidget):
|
|||
form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
form_layout.setSpacing(10)
|
||||
|
||||
# Input Form Fields
|
||||
self.txt_english = QLineEdit()
|
||||
self.txt_english.setPlaceholderText("Enter English phrase or word...")
|
||||
self.txt_english.setPlaceholderText("Enter English phrase (filters grid real-time)...")
|
||||
self.txt_english.setStyleSheet("padding: 6px; font-size: 14px;")
|
||||
self.txt_english.textChanged.connect(self.handle_live_filter)
|
||||
self.txt_english.textChanged.connect(self.apply_live_grid_filter)
|
||||
|
||||
self.txt_spanish = QLineEdit()
|
||||
self.txt_spanish.setPlaceholderText("Introduce la frase en español...")
|
||||
self.txt_spanish.setPlaceholderText("Enter Spanish phrase (filters grid real-time)...")
|
||||
self.txt_spanish.setStyleSheet("padding: 6px; font-size: 14px;")
|
||||
self.txt_spanish.textChanged.connect(self.handle_live_filter)
|
||||
self.txt_spanish.textChanged.connect(self.apply_live_grid_filter)
|
||||
|
||||
self.txt_context = QLineEdit()
|
||||
self.txt_context.setPlaceholderText("e.g., Camino 2027, Café, Market conversation...")
|
||||
self.txt_context.setPlaceholderText("Context e.g., Camino 2027 (filters grid real-time)...")
|
||||
self.txt_context.setStyleSheet("padding: 6px; font-size: 14px;")
|
||||
self.txt_context.textChanged.connect(self.handle_live_filter)
|
||||
self.txt_context.textChanged.connect(self.apply_live_grid_filter)
|
||||
|
||||
# NEW: Tags input field with live filter connection
|
||||
self.txt_tags = QLineEdit()
|
||||
self.txt_tags.setPlaceholderText("e.g., verb, greeting, subjunctive, travel...")
|
||||
self.txt_tags.setPlaceholderText("Comma separated tags (filters grid real-time)...")
|
||||
self.txt_tags.setStyleSheet("padding: 6px; font-size: 14px;")
|
||||
self.txt_tags.textChanged.connect(self.handle_live_filter)
|
||||
self.txt_tags.textChanged.connect(self.apply_live_grid_filter)
|
||||
|
||||
self.txt_notes = QLineEdit()
|
||||
self.txt_notes.setPlaceholderText("Grammar rules, formal vs informal nuances...")
|
||||
self.txt_notes.setStyleSheet("padding: 6px; font-size: 14px;")
|
||||
# Modal Editor Row Buttons
|
||||
editor_buttons_layout = QHBoxLayout()
|
||||
self.btn_edit_notes = QPushButton("📝 Edit Notes Block")
|
||||
self.btn_edit_notes.clicked.connect(self.open_notes_editor)
|
||||
|
||||
# Mount fields onto Form Layout
|
||||
form_layout.addRow(QLabel("<b>English Text:</b>"), self.txt_english)
|
||||
form_layout.addRow(QLabel("<b>Spanish Text:</b>"), self.txt_spanish)
|
||||
self.btn_edit_anki_notes = QPushButton("🗂️ Edit Anki Notes Block")
|
||||
self.btn_edit_anki_notes.clicked.connect(self.open_anki_notes_editor)
|
||||
|
||||
editor_buttons_layout.addWidget(self.btn_edit_notes)
|
||||
editor_buttons_layout.addWidget(self.btn_edit_anki_notes)
|
||||
|
||||
form_layout.addRow(QLabel("<b>English Phrase:</b>"), self.txt_english)
|
||||
form_layout.addRow(QLabel("<b>Spanish Translation:</b>"), self.txt_spanish)
|
||||
form_layout.addRow(QLabel("<b>Source Context:</b>"), self.txt_context)
|
||||
form_layout.addRow(QLabel("<b>Tags:</b>"), self.txt_tags)
|
||||
form_layout.addRow(QLabel("<b>Historical Notes:</b>"), self.txt_notes)
|
||||
form_layout.addRow(QLabel("<b>Extended Data Fields:</b>"), editor_buttons_layout)
|
||||
|
||||
main_layout.addWidget(form_container)
|
||||
|
||||
# --- SECTION 2: AUDIO PREVIEW ACTION ROW ---
|
||||
audio_layout = QHBoxLayout()
|
||||
audio_layout.setSpacing(15)
|
||||
# --- INLINE AUDIO CONTROL + GENDER SELECTION PANEL ---
|
||||
audio_panel = QHBoxLayout()
|
||||
audio_panel.setSpacing(15)
|
||||
|
||||
self.btn_play_en = QPushButton("🔊 Test English Voice")
|
||||
self.btn_play_en.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.btn_play_en.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #E67E22;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:hover { background-color: #D35400; }
|
||||
""")
|
||||
self.btn_play_en.clicked.connect(self.preview_english_audio)
|
||||
self.btn_test_en = QPushButton("🔊 Test English Voice")
|
||||
self.btn_test_en.clicked.connect(self.audition_english)
|
||||
|
||||
self.btn_play_es = QPushButton("🔊 Test Mónica (Spanish)")
|
||||
self.btn_play_es.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.btn_play_es.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #9B59B6;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:hover { background-color: #8E44AD; }
|
||||
""")
|
||||
self.btn_play_es.clicked.connect(self.preview_spanish_audio)
|
||||
self.btn_test_es = QPushButton("🔊 Test Spanish Voice")
|
||||
self.btn_test_es.clicked.connect(self.audition_spanish)
|
||||
|
||||
audio_layout.addWidget(self.btn_play_en)
|
||||
audio_layout.addWidget(self.btn_play_es)
|
||||
audio_layout.addStretch()
|
||||
gender_label = QLabel("<b>Speaker Gender:</b>")
|
||||
self.rb_female = QRadioButton("Female")
|
||||
self.rb_male = QRadioButton("Male")
|
||||
self.rb_female.setChecked(True)
|
||||
|
||||
main_layout.addLayout(audio_layout)
|
||||
self.gender_group = QButtonGroup(self)
|
||||
self.gender_group.addButton(self.rb_female)
|
||||
self.gender_group.addButton(self.rb_male)
|
||||
|
||||
# --- SECTION 3: DATA COMMIT CONTROL BAR ---
|
||||
control_layout = QHBoxLayout()
|
||||
control_layout.setSpacing(15)
|
||||
audio_panel.addWidget(self.btn_test_en)
|
||||
audio_panel.addWidget(self.btn_test_es)
|
||||
audio_panel.addSpacing(20)
|
||||
audio_panel.addWidget(gender_label)
|
||||
audio_panel.addWidget(self.rb_female)
|
||||
audio_panel.addWidget(self.rb_male)
|
||||
audio_panel.addStretch()
|
||||
|
||||
self.btn_save = QPushButton("Save Translation Record")
|
||||
self.btn_save.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.btn_save.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2980B9;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QPushButton:hover { background-color: #1F618D; }
|
||||
""")
|
||||
self.btn_save.clicked.connect(self.commit_translation_record)
|
||||
main_layout.addLayout(audio_panel)
|
||||
|
||||
self.btn_new_record = QPushButton("Create New Record")
|
||||
self.btn_new_record.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.btn_new_record.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #27AE60;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QPushButton:hover { background-color: #219653; }
|
||||
""")
|
||||
self.btn_new_record.clicked.connect(self.prepare_for_new_record)
|
||||
# --- ACTION CONTROL BAR ---
|
||||
actions_layout = QHBoxLayout()
|
||||
self.btn_save_record = QPushButton("📥 Save Transaction")
|
||||
self.btn_save_record.clicked.connect(self.commit_form_entry)
|
||||
self.btn_save_record.setStyleSheet("background-color: #27AE60; color: white; font-weight: bold; padding: 8px 16px;")
|
||||
|
||||
self.btn_clear = QPushButton("Clear Fields")
|
||||
self.btn_clear.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.btn_clear.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #BDC3C7;
|
||||
color: #34495E;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QPushButton:hover { background-color: #95A5A6; }
|
||||
""")
|
||||
self.btn_clear.clicked.connect(self.clear_all_fields_manually)
|
||||
self.btn_clear_form = QPushButton("🧹 Reset Fields")
|
||||
self.btn_clear_form.clicked.connect(self.clear_form_fields)
|
||||
|
||||
control_layout.addWidget(self.btn_save)
|
||||
control_layout.addWidget(self.btn_new_record)
|
||||
control_layout.addWidget(self.btn_clear)
|
||||
control_layout.addStretch()
|
||||
self.btn_delete_record = QPushButton("🗑️ Delete Selected")
|
||||
self.btn_delete_record.clicked.connect(self.remove_target_record)
|
||||
self.btn_delete_record.setStyleSheet("background-color: #C0392B; color: white;")
|
||||
|
||||
main_layout.addLayout(control_layout)
|
||||
actions_layout.addWidget(self.btn_save_record)
|
||||
actions_layout.addWidget(self.btn_clear_form)
|
||||
actions_layout.addWidget(self.btn_delete_record)
|
||||
actions_layout.addStretch()
|
||||
main_layout.addLayout(actions_layout)
|
||||
|
||||
# --- SECTION 4: DATALIST DISPLAY REGION ---
|
||||
# --- VIEWPORT GRID TABLE ---
|
||||
self.table = QTableWidget()
|
||||
self.table.setColumnCount(6) # Increased to 6 to display Tags column
|
||||
self.table.setHorizontalHeaderLabels(["ID", "English Phrase", "Spanish Translation", "Context", "Tags", "Notes"])
|
||||
self.table.setColumnCount(6)
|
||||
self.table.setHorizontalHeaderLabels(["ID", "English", "Spanish", "Context", "Tags", "Gender"])
|
||||
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self.table.cellClicked.connect(self.populate_form_from_grid)
|
||||
|
||||
# Wire up row selection change signals to auto-populate form
|
||||
self.table.itemSelectionChanged.connect(self.handle_row_selection)
|
||||
|
||||
# Tweak display headers to scale nicely
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Interactive)
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Interactive) # Tags Header
|
||||
header.setSectionResizeMode(5, QHeaderView.ResizeMode.Interactive) # Notes Header
|
||||
|
||||
main_layout.addWidget(self.table)
|
||||
|
||||
# Master cache of unfiltered row records to enable instant filtering loops
|
||||
self.all_cached_records = []
|
||||
self.reload_table_display()
|
||||
|
||||
@pyqtSlot()
|
||||
def preview_english_audio(self):
|
||||
"""Auditions current text state inside the English text box field."""
|
||||
text = self.txt_english.text().strip()
|
||||
if text:
|
||||
subprocess.Popen(["say", text])
|
||||
def open_notes_editor(self):
|
||||
dlg = TextEditorDialog("Edit Grammar / Core Notes Block", self.current_notes_content, self)
|
||||
if dlg.exec():
|
||||
self.current_notes_content = dlg.get_text()
|
||||
print(f"[DEBUG DIALOG CLOSE] Notes Block updated in memory: {self.current_notes_content}")
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def preview_spanish_audio(self):
|
||||
"""Auditions current text state inside the Spanish text box field using Mónica."""
|
||||
text = self.txt_spanish.text().strip()
|
||||
if text:
|
||||
subprocess.Popen(["say", "-v", "Monica", text])
|
||||
def open_anki_notes_editor(self):
|
||||
dlg = TextEditorDialog("Edit Anki Specialized Meta Field", self.current_anki_notes_content, self)
|
||||
if dlg.exec():
|
||||
self.current_anki_notes_content = dlg.get_text()
|
||||
print(f"[DEBUG DIALOG CLOSE] Anki Notes Block updated in memory: {self.current_anki_notes_content}")
|
||||
|
||||
@pyqtSlot()
|
||||
def handle_live_filter(self):
|
||||
"""Filters the visible entries based on English, Spanish, Context, and Tags criteria."""
|
||||
# Temporary block signals to prevent selection loops from fighting text changes
|
||||
self.table.blockSignals(True)
|
||||
|
||||
filter_en = self.txt_english.text().lower().strip()
|
||||
filter_es = self.txt_spanish.text().lower().strip()
|
||||
filter_ctx = self.txt_context.text().lower().strip()
|
||||
filter_tag = self.txt_tags.text().lower().strip() # Capture tag text query
|
||||
|
||||
self.table.setRowCount(0)
|
||||
visible_row_index = 0
|
||||
|
||||
for row in self.all_cached_records:
|
||||
val_en = (row["en_text"] or "").lower()
|
||||
val_es = (row["es_text"] or "").lower()
|
||||
val_ctx = (row["source_context"] or "").lower()
|
||||
val_tag = (row["tags"] or "").lower() # Extract tags criteria
|
||||
|
||||
# Look for explicit matching conditions across all 4 entry variables
|
||||
if (filter_en in val_en) and (filter_es in val_es) and (filter_ctx in val_ctx) and (filter_tag in val_tag):
|
||||
self.table.insertRow(visible_row_index)
|
||||
|
||||
item_id = QTableWidgetItem(str(row["translation_id"]))
|
||||
item_en = QTableWidgetItem(row["en_text"])
|
||||
item_es = QTableWidgetItem(row["es_text"])
|
||||
item_ctx = QTableWidgetItem(row["source_context"] or "")
|
||||
item_tag = QTableWidgetItem(row["tags"] or "")
|
||||
item_nts = QTableWidgetItem(row["notes"] or "")
|
||||
|
||||
item_id.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.table.setItem(visible_row_index, 0, item_id)
|
||||
self.table.setItem(visible_row_index, 1, item_en)
|
||||
self.table.setItem(visible_row_index, 2, item_es)
|
||||
self.table.setItem(visible_row_index, 3, item_ctx)
|
||||
self.table.setItem(visible_row_index, 4, item_tag)
|
||||
self.table.setItem(visible_row_index, 5, item_nts)
|
||||
|
||||
# If we have an active editing ID, highlight that specific row during re-renders
|
||||
if self.selected_translation_id == row["translation_id"]:
|
||||
self.table.selectRow(visible_row_index)
|
||||
|
||||
visible_row_index += 1
|
||||
|
||||
self.table.blockSignals(False)
|
||||
|
||||
@pyqtSlot()
|
||||
def handle_row_selection(self):
|
||||
"""Populates the input forms when a user clicks a row in the table view."""
|
||||
selected_ranges = self.table.selectedRanges()
|
||||
if not selected_ranges:
|
||||
return
|
||||
|
||||
row_idx = selected_ranges[0].topRow()
|
||||
id_item = self.table.item(row_idx, 0)
|
||||
if not id_item:
|
||||
return
|
||||
|
||||
target_id = int(id_item.text())
|
||||
|
||||
# Locate item match within memory cache store elements
|
||||
record = next((r for r in self.all_cached_records if r["translation_id"] == target_id), None)
|
||||
if record:
|
||||
# Block line edit text tracking temporarily so populating fields doesn't trigger filter loops
|
||||
self.txt_english.blockSignals(True)
|
||||
self.txt_spanish.blockSignals(True)
|
||||
self.txt_context.blockSignals(True)
|
||||
self.txt_tags.blockSignals(True)
|
||||
self.txt_notes.blockSignals(True)
|
||||
|
||||
self.selected_translation_id = record["translation_id"]
|
||||
self.txt_english.setText(record["en_text"])
|
||||
self.txt_spanish.setText(record["es_text"])
|
||||
self.txt_context.setText(record["source_context"] or "")
|
||||
self.txt_tags.setText(record["tags"] or "")
|
||||
self.txt_notes.setText(record["notes"] or "")
|
||||
|
||||
self.txt_english.blockSignals(False)
|
||||
self.txt_spanish.blockSignals(False)
|
||||
self.txt_context.blockSignals(False)
|
||||
self.txt_tags.blockSignals(False)
|
||||
self.txt_notes.blockSignals(False)
|
||||
|
||||
@pyqtSlot()
|
||||
def prepare_for_new_record(self):
|
||||
"""Clears selection state so next click on 'Save' inserts fresh rows without scrubbing fields."""
|
||||
self.selected_translation_id = None
|
||||
self.table.blockSignals(True)
|
||||
self.table.clearSelection()
|
||||
self.table.blockSignals(False)
|
||||
QMessageBox.information(self, "Status Shift", "Ready to insert a new record using the current field content.")
|
||||
|
||||
@pyqtSlot()
|
||||
def commit_translation_record(self):
|
||||
"""Saves current text blocks. Dynamically detects insert vs edit based on selections."""
|
||||
en_text = self.txt_english.text().strip()
|
||||
es_text = self.txt_spanish.text().strip()
|
||||
context = self.txt_context.text().strip()
|
||||
tags = self.txt_tags.text().strip()
|
||||
notes = self.txt_notes.text().strip()
|
||||
|
||||
if not en_text or not es_text:
|
||||
QMessageBox.warning(self, "Validation Alert", "Both English and Spanish base text blocks are required.")
|
||||
return
|
||||
|
||||
try:
|
||||
if self.selected_translation_id is not None:
|
||||
# database.update_translation_record signature: (translation_id, es_text, en_text, source_context, tags, notes)
|
||||
database.update_translation_record(
|
||||
self.selected_translation_id, es_text, en_text, context, tags, notes
|
||||
)
|
||||
else:
|
||||
# database.insert_translation_record signature: (es_text, en_text, source_context, tags, notes)
|
||||
database.insert_translation_record(
|
||||
es_text, en_text, context, tags, notes
|
||||
)
|
||||
|
||||
# Clear pointer state values on successful writing commits
|
||||
self.selected_translation_id = None
|
||||
|
||||
# Wipe inputs cleanly and sync UI layers
|
||||
self.clear_input_fields()
|
||||
self.reload_table_display()
|
||||
self.data_mutated.emit()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Database Commit Safeguard", f"Failed writing database operations:\n{str(e)}")
|
||||
|
||||
@pyqtSlot()
|
||||
def clear_all_fields_manually(self):
|
||||
"""Clears explicit states alongside visual row highlights simultaneously."""
|
||||
self.selected_translation_id = None
|
||||
self.table.blockSignals(True)
|
||||
self.table.clearSelection()
|
||||
self.table.blockSignals(False)
|
||||
self.clear_input_fields()
|
||||
self.reload_table_display()
|
||||
|
||||
def clear_input_fields(self):
|
||||
"""Flushes transient text inside line editors without running filtering rules."""
|
||||
def clear_form_fields(self):
|
||||
self.txt_english.blockSignals(True)
|
||||
self.txt_spanish.blockSignals(True)
|
||||
self.txt_context.blockSignals(True)
|
||||
self.txt_tags.blockSignals(True)
|
||||
self.txt_notes.blockSignals(True)
|
||||
|
||||
self.selected_translation_id = None
|
||||
self.txt_english.clear()
|
||||
self.txt_spanish.clear()
|
||||
self.txt_context.clear()
|
||||
self.txt_tags.clear()
|
||||
self.txt_notes.clear()
|
||||
self.current_notes_content = ""
|
||||
self.current_anki_notes_content = ""
|
||||
self.rb_female.setChecked(True)
|
||||
|
||||
self.txt_english.blockSignals(False)
|
||||
self.txt_spanish.blockSignals(False)
|
||||
self.txt_context.blockSignals(False)
|
||||
self.txt_tags.blockSignals(False)
|
||||
self.txt_notes.blockSignals(False)
|
||||
|
||||
self.apply_live_grid_filter()
|
||||
|
||||
@pyqtSlot()
|
||||
def commit_form_entry(self):
|
||||
en_t = self.txt_english.text().strip()
|
||||
es_t = self.txt_spanish.text().strip()
|
||||
ctx_t = self.txt_context.text().strip()
|
||||
tag_t = self.txt_tags.text().strip()
|
||||
gender_t = "Male" if self.rb_male.isChecked() else "Female"
|
||||
|
||||
if not en_t or not es_t:
|
||||
QMessageBox.warning(self, "Validation Alert", "English and Spanish phrase properties cannot remain blank.")
|
||||
return
|
||||
|
||||
print(f"\n[DEBUG DATABASE WRITE]")
|
||||
print(f" ID Selected: {self.selected_translation_id}")
|
||||
print(f" English: {en_t}")
|
||||
print(f" Spanish: {es_t}")
|
||||
print(f" Context: {ctx_t}")
|
||||
print(f" Tags: {tag_t}")
|
||||
print(f" Notes: {self.current_notes_content}")
|
||||
print(f" Anki Notes: {self.current_anki_notes_content}")
|
||||
print(f" Gender: {gender_t}\n")
|
||||
|
||||
|
||||
# Fixed argument sequence to match Beekeeper schema (gender position 7, anki_notes position 8)
|
||||
if self.selected_translation_id is None:
|
||||
database.insert_translation_record(es_t, en_t, ctx_t, tag_t, self.current_notes_content, gender_t, self.current_anki_notes_content)
|
||||
else:
|
||||
database.update_translation_record(self.selected_translation_id, es_t, en_t, ctx_t, tag_t, self.current_notes_content, gender_t, self.current_anki_notes_content)
|
||||
|
||||
self.clear_form_fields()
|
||||
self.reload_table_display()
|
||||
self.data_mutated.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def remove_target_record(self):
|
||||
if self.selected_translation_id is None:
|
||||
QMessageBox.warning(self, "Selection Missing", "Please select a row from the grid viewport before attempting deletion.")
|
||||
return
|
||||
|
||||
confirm = QMessageBox.question(
|
||||
self,
|
||||
"Confirm Deletion",
|
||||
"Are you sure you want to permanently delete this translation record?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if confirm == QMessageBox.StandardButton.Yes:
|
||||
database.delete_translation_record(self.selected_translation_id)
|
||||
self.clear_form_fields()
|
||||
self.reload_table_display()
|
||||
self.data_mutated.emit()
|
||||
|
||||
def populate_form_from_grid(self, row, col):
|
||||
self.selected_translation_id = int(self.table.item(row, 0).text())
|
||||
record = database.get_translation_by_id(self.selected_translation_id)
|
||||
|
||||
if record:
|
||||
self.txt_english.blockSignals(True)
|
||||
self.txt_spanish.blockSignals(True)
|
||||
self.txt_context.blockSignals(True)
|
||||
self.txt_tags.blockSignals(True)
|
||||
|
||||
self.txt_english.setText(record["en_text"])
|
||||
self.txt_spanish.setText(record["es_text"])
|
||||
self.txt_context.setText(record.get("source_context", ""))
|
||||
self.txt_tags.setText(record.get("tags", ""))
|
||||
self.current_notes_content = record.get("notes", "")
|
||||
self.current_anki_notes_content = record.get("anki_notes", "")
|
||||
|
||||
if record.get("gender") == "Male":
|
||||
self.rb_male.setChecked(True)
|
||||
else:
|
||||
self.rb_female.setChecked(True)
|
||||
|
||||
self.txt_english.blockSignals(False)
|
||||
self.txt_spanish.blockSignals(False)
|
||||
self.txt_context.blockSignals(False)
|
||||
self.txt_tags.blockSignals(False)
|
||||
|
||||
def reload_table_display(self):
|
||||
"""Refetches database rows and populates the master dashboard grid view."""
|
||||
self.table.blockSignals(True)
|
||||
self.cached_records = database.get_all_translations_explicit()
|
||||
self.apply_live_grid_filter()
|
||||
|
||||
@pyqtSlot()
|
||||
def apply_live_grid_filter(self):
|
||||
filter_en = self.txt_english.text().lower().strip()
|
||||
filter_es = self.txt_spanish.text().lower().strip()
|
||||
filter_ctx = self.txt_context.text().lower().strip()
|
||||
filter_tags = self.txt_tags.text().lower().strip()
|
||||
|
||||
self.table.setRowCount(0)
|
||||
visible_row_idx = 0
|
||||
|
||||
# Keep internal reference arrays synced cleanly
|
||||
self.all_cached_records = database.get_all_translations_explicit()
|
||||
for r in self.cached_records:
|
||||
match_en = filter_en in (r.get("en_text") or "").lower()
|
||||
match_es = filter_es in (r.get("es_text") or "").lower()
|
||||
match_ctx = filter_ctx in (r.get("source_context") or "").lower()
|
||||
match_tags = filter_tags in (r.get("tags") or "").lower()
|
||||
|
||||
for idx, row in enumerate(self.all_cached_records):
|
||||
self.table.insertRow(idx)
|
||||
if match_en and match_es and match_ctx and match_tags:
|
||||
self.table.insertRow(visible_row_idx)
|
||||
self.table.setItem(visible_row_idx, 0, QTableWidgetItem(str(r["translation_id"])))
|
||||
self.table.setItem(visible_row_idx, 1, QTableWidgetItem(r["en_text"]))
|
||||
self.table.setItem(visible_row_idx, 2, QTableWidgetItem(r["es_text"]))
|
||||
self.table.setItem(visible_row_idx, 3, QTableWidgetItem(r.get("source_context", "")))
|
||||
self.table.setItem(visible_row_idx, 4, QTableWidgetItem(r.get("tags", "")))
|
||||
self.table.setItem(visible_row_idx, 5, QTableWidgetItem(r.get("gender", "Female")))
|
||||
visible_row_idx += 1
|
||||
|
||||
item_id = QTableWidgetItem(str(row["translation_id"]))
|
||||
item_en = QTableWidgetItem(row["en_text"])
|
||||
item_es = QTableWidgetItem(row["es_text"])
|
||||
item_ctx = QTableWidgetItem(row["source_context"] or "")
|
||||
item_tag = QTableWidgetItem(row["tags"] or "")
|
||||
item_nts = QTableWidgetItem(row["notes"] or "")
|
||||
def _async_edge_speech_worker(self, text, voice, rate_modifier):
|
||||
"""Background thread worker to download neural audio and play it without freezing the UI."""
|
||||
async def stream_audio():
|
||||
temp_file = os.path.join(tempfile.gettempdir(), "sandbox_audition.mp3")
|
||||
try:
|
||||
communicate = edge_tts.Communicate(text=text, voice=voice, rate=rate_modifier)
|
||||
await communicate.save(temp_file)
|
||||
if os.path.exists(temp_file):
|
||||
subprocess.run(["afplay", temp_file])
|
||||
except Exception as e:
|
||||
print(f"Sandbox Audition Error: {e}")
|
||||
|
||||
item_id.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
asyncio.run(stream_audio())
|
||||
|
||||
self.table.setItem(idx, 0, item_id)
|
||||
self.table.setItem(idx, 1, item_en)
|
||||
self.table.setItem(idx, 2, item_es)
|
||||
self.table.setItem(idx, 3, item_ctx)
|
||||
self.table.setItem(idx, 4, item_tag)
|
||||
self.table.setItem(idx, 5, item_nts)
|
||||
@pyqtSlot()
|
||||
def audition_english(self):
|
||||
txt = self.txt_english.text().strip()
|
||||
if not txt:
|
||||
return
|
||||
|
||||
self.table.blockSignals(False)
|
||||
settings = database.load_all_settings() or {}
|
||||
# Fixed: Changed lookup from "playback_speed_multiplier" to "tts_playback_speed"
|
||||
config_speed = settings.get("tts_playback_speed", "1.0")
|
||||
try:
|
||||
pct = int((float(config_speed) - 1.0) * 100)
|
||||
rate_string = f"{'+' if pct >= 0 else ''}{pct}%"
|
||||
except Exception:
|
||||
rate_string = "+0%"
|
||||
|
||||
threading.Thread(
|
||||
target=self._async_edge_speech_worker,
|
||||
args=(txt, "en-US-EmmaNeural", rate_string),
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
@pyqtSlot()
|
||||
def audition_spanish(self):
|
||||
txt = self.txt_spanish.text().strip()
|
||||
if not txt:
|
||||
return
|
||||
|
||||
# Dynamically switch between neural voices depending on the active form state radio selection
|
||||
voice = "es-ES-AlvaroNeural" if self.rb_male.isChecked() else "es-ES-ElviraNeural"
|
||||
|
||||
settings = database.load_all_settings() or {}
|
||||
config_speed = settings.get("tts_playback_speed", "1.0")
|
||||
try:
|
||||
pct = int((float(config_speed) - 1.0) * 100)
|
||||
print(f"[DEBUG Sandbox Speech] config_speed (Raw String): {config_speed}")
|
||||
print(f"[DEBUG Sandbox Speech] Calculated pct (Integer): {pct}")
|
||||
rate_string = f"{'+' if pct >= 0 else ''}{pct}%"
|
||||
except Exception:
|
||||
print(f"[DEBUG Sandbox Speech] Failed to parse speed calculation")
|
||||
rate_string = "+0%"
|
||||
|
||||
threading.Thread(
|
||||
target=self._async_edge_speech_worker,
|
||||
args=(txt, voice, rate_string),
|
||||
daemon=True
|
||||
).start()
|
||||
Loading…
Reference in a new issue