this version replaced edge-tts with kokoro locally based tts model

This commit is contained in:
stephen 2026-10-17 12:13:03 +11:00
parent 4a4d16c093
commit 2346bacd5d
37 changed files with 383 additions and 1263 deletions

5
.gitignore vendored
View file

@ -25,4 +25,7 @@ media/*
# IDE and Environment Configurations
.vscode/
.idea/
.env
.env
*.onnx
*.bin

View file

@ -0,0 +1,51 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='Spanish Voice Trainer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['app.icns'],
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='Spanish Voice Trainer',
)
app = BUNDLE(
coll,
name='Spanish Voice Trainer.app',
icon='app.icns',
bundle_identifier=None,
)

View file

@ -1,32 +1,20 @@
# anki_exporter.py
import os
import tempfile
import asyncio
import time
import genanki
import edge_tts
import database
from tts_utils import parse_text_for_edgetts, get_configured_tts_rate
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
from tts_utils import parse_text_for_edgetts
from kokoro_utils import generate_local_speech
def compile_anki_package(records, output_path, deck_name):
"""
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.
speed multipliers from active configurations using local Kokoro TTS.
Injects sort_order as field 0 for structured card sequencing.
"""
# Generate deterministic positive 32-bit integers from deck name and model name
# to avoid collisions across different decks while keeping imports stable
deck_id = abs(hash(deck_name)) % (2**31)
model_id = abs(hash("Spanish Bidirectional Multi-Note HTML Model")) % (2**31)
@ -35,7 +23,10 @@ def compile_anki_package(records, output_path, deck_name):
# Global Configuration Pace Resolver Mapping
settings = database.load_all_settings() or {}
rate_string = get_configured_tts_rate(settings)
try:
speed_multiplier = float(settings.get("tts_playback_speed", 1.0))
except (TypeError, ValueError):
speed_multiplier = 1.0
anki_model = genanki.Model(
model_id,
@ -52,8 +43,8 @@ def compile_anki_package(records, output_path, deck_name):
{
'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>',
'<div style="font-family: Arial; font-size: 20px; text-align: center; color: #34495E;"><b>{{EnglishText}}</b></div><br>'
'<div style="text-align: center;">{{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>'
'{{#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>'
@ -62,8 +53,8 @@ def compile_anki_package(records, output_path, deck_name):
{
'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>',
'<div style="font-family: Arial; font-size: 26px; text-align: center; color: #2980B9; font-weight: bold;"><b>{{SpanishText}}</b></div><br>'
'<div style="text-align: center;">{{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>'
'{{#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>'
@ -75,12 +66,6 @@ def compile_anki_package(records, output_path, deck_name):
deck = genanki.Deck(deck_id, deck_name)
media_files_to_pack = []
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
with tempfile.TemporaryDirectory() as tmpdir:
for idx, record in enumerate(records):
en_raw = record["en_text"]
@ -96,38 +81,57 @@ def compile_anki_package(records, output_path, deck_name):
en_tts_text = parse_text_for_edgetts(en_raw)
es_tts_text = parse_text_for_edgetts(es_raw)
# 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"
# Map Kokoro voices matching gender settings
spanish_voice = "em_alex" if gender_flag == "Male" else "ef_dora"
english_voice = "af_bella"
raw_tags = record.get("tags") or ""
note_tags = [t.strip().replace(" ", "_") for t in raw_tags.split(",") if t.strip()]
# Safely isolate the raw text/HTML data string down to Anki notes field
anki_notes_html = record['anki_notes'].strip() if record.get('anki_notes') else ""
# Unique Media Filenames combining execution timestamp and index
en_audio_filename = f"edge_en_{run_prefix}_{idx}.mp3"
es_audio_filename = f"edge_es_{run_prefix}_{idx}.mp3"
# Unique Media Filenames combining execution timestamp and index (.wav output from soundfile)
en_audio_filename = f"kokoro_en_{run_prefix}_{idx}.wav"
es_audio_filename = f"kokoro_es_{run_prefix}_{idx}.wav"
en_audio_path = os.path.join(tmpdir, en_audio_filename)
es_audio_path = os.path.join(tmpdir, es_audio_filename)
# English Audio Synthesis
if en_tts_text.strip() and loop.run_until_complete(generate_edge_audio(en_tts_text, english_voice, en_audio_path, rate_string)):
media_files_to_pack.append(en_audio_path)
en_audio_field = f"[sound:{en_audio_filename}]"
else:
# English Audio Synthesis via Kokoro
try:
if en_tts_text.strip():
generated_path = generate_local_speech(en_tts_text, voice_name=english_voice, speed=speed_multiplier)
if os.path.exists(generated_path):
# Copy temp file to designated exporter temp directory path
import shutil
shutil.copyfile(generated_path, en_audio_path)
media_files_to_pack.append(en_audio_path)
en_audio_field = f"[sound:{en_audio_filename}]"
else:
en_audio_field = ""
else:
en_audio_field = ""
except Exception as e:
print(f"[Anki Exporter] English TTS error for card {idx}: {e}")
en_audio_field = ""
# Spanish Audio Synthesis
if es_tts_text.strip() and loop.run_until_complete(generate_edge_audio(es_tts_text, spanish_voice, es_audio_path, rate_string)):
media_files_to_pack.append(es_audio_path)
es_audio_field = f"[sound:{es_audio_filename}]"
else:
# Spanish Audio Synthesis via Kokoro
try:
if es_tts_text.strip():
generated_path = generate_local_speech(es_tts_text, voice_name=spanish_voice, speed=speed_multiplier)
if os.path.exists(generated_path):
import shutil
shutil.copyfile(generated_path, es_audio_path)
media_files_to_pack.append(es_audio_path)
es_audio_field = f"[sound:{es_audio_filename}]"
else:
es_audio_field = ""
else:
es_audio_field = ""
except Exception as e:
print(f"[Anki Exporter] Spanish TTS error for card {idx}: {e}")
es_audio_field = ""
# Clean sequential matching fields array matching schema mapping above
note = genanki.Note(
model=anki_model,
fields=[sort_order_str, en_raw, es_raw, anki_notes_html, en_audio_field, es_audio_field],
@ -135,7 +139,7 @@ def compile_anki_package(records, output_path, deck_name):
)
deck.add_note(note)
# Build Package while media assets are guaranteed contextually active inside tmpdir
package = genanki.Package(deck)
package.media_files = media_files_to_pack
package.write_to_file(output_path)
package.write_to_file(output_path)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

Before

Width:  |  Height:  |  Size: 13 MiB

After

Width:  |  Height:  |  Size: 13 MiB

View file

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -64,4 +64,11 @@ tree --gitignore
* **Build & Bundle Outputs:** `build/`, `dist/`, `main.app`
* **Python Caches:** `__pycache__/`, `*.pyc`
* **Static / Temp Media:** `*.png`, `*.jpg`, `*.pdf`, `app_icon.iconset/`
* **Legacy / Backups:** `database_legacy/` (containing database backups like `*.db`)
* **Legacy / Backups:** `database_legacy/` (containing database backups like `*.db`)
There is a problem with edge-tts
so we are going to install kokoro
39_spanish-voice-trainer % git tag -a V37.0 -m "this version replaced edge-tts with kokoro locally based tts model"
(139_spanish-voice-trainer) stephenlohning

Binary file not shown.

47
kokoro_utils.py Normal file
View file

@ -0,0 +1,47 @@
# kokoro_utils.py
import os
import tempfile
import soundfile as sf
from kokoro_onnx import Kokoro
# Resolve paths relative to project root
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODEL_PATH = os.path.join(BASE_DIR, "kokoro-v1.0.onnx")
VOICES_PATH = os.path.join(BASE_DIR, "voices-v1.0.bin")
_kokoro_instance = None
def get_kokoro_engine():
global _kokoro_instance
if _kokoro_instance is None:
if os.path.exists(MODEL_PATH) and os.path.exists(VOICES_PATH):
_kokoro_instance = Kokoro(MODEL_PATH, VOICES_PATH)
return _kokoro_instance
def generate_local_speech(text: str, voice_name: str = "af_bella", speed: float = 1.0) -> str:
"""
Synthesizes speech locally using Kokoro and saves to a temp file,
returning the path to the generated audio file with correct language routing.
"""
engine = get_kokoro_engine()
if not engine:
raise FileNotFoundError(
f"Kokoro model files not found at root directory.\nExpected:\n- {MODEL_PATH}\n- {VOICES_PATH}"
)
# Route language code explicitly based on voice prefix
if voice_name.startswith("es") or voice_name.startswith("ef") or voice_name.startswith("em"):
lang_code = "es"
else:
lang_code = "en-us"
samples, sample_rate = engine.create(
text,
voice=voice_name,
speed=speed,
lang=lang_code
)
temp_file = os.path.join(tempfile.gettempdir(), "kokoro_review_audio.wav")
sf.write(temp_file, samples, sample_rate)
return temp_file

129
main.py
View file

@ -1,7 +1,12 @@
# main.py
import sys
import os
from PyQt6.QtWidgets import QMainWindow, QTabWidget, QApplication, QMessageBox, QVBoxLayout, QWidget
import shutil
from PyQt6.QtWidgets import (
QMainWindow, QTabWidget, QApplication, QMessageBox,
QVBoxLayout, QWidget, QFileDialog
)
from PyQt6.QtGui import QAction, QKeySequence
from PyQt6.QtCore import pyqtSlot
# Import our unified data layer
@ -19,7 +24,7 @@ import video_generator
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Castilian Spanish Voice Trainer")
self.setWindowTitle("Spanish Voice Trainer")
self.setMinimumSize(900, 650)
# 1. Ensure database structure exists on startup
@ -47,9 +52,121 @@ class MainWindow(QMainWindow):
self.tab_widget.addTab(self.review_tab, "Flashcard Review")
self.tab_widget.addTab(self.settings_tab, "Configuration Settings")
# 6. Wire up the cross-module communication channels (Signals)
# 6. Build application Menu Bar
self.create_menu_bar()
# 7. Wire up the cross-module communication channels (Signals)
self.wire_application_signals()
def create_menu_bar(self):
"""Constructs native macOS Menu Bar integrating directly with the primary system process menu."""
menu_bar = self.menuBar()
# --- 1. APP MENU (Spanish Voice Trainer) ---
# An empty string title merges actions directly into the native primary macOS Application Menu
app_menu = menu_bar.addMenu("")
about_action = QAction("About Spanish Voice Trainer", self)
about_action.setMenuRole(QAction.MenuRole.AboutRole)
about_action.triggered.connect(self.show_about_dialog)
app_menu.addAction(about_action)
update_action = QAction("Check for Updates...", self)
update_action.setMenuRole(QAction.MenuRole.ApplicationSpecificRole)
update_action.triggered.connect(self.check_for_updates)
app_menu.addAction(update_action)
quit_action = QAction("Quit Spanish Voice Trainer", self)
quit_action.setMenuRole(QAction.MenuRole.QuitRole)
quit_action.setShortcut(QKeySequence.StandardKey.Quit)
quit_action.triggered.connect(QApplication.instance().quit)
app_menu.addAction(quit_action)
# --- 2. TOOLS MENU ---
tools_menu = menu_bar.addMenu("Tools")
backup_action = QAction("Backup Database...", self)
backup_action.triggered.connect(self.handle_backup_database)
tools_menu.addAction(backup_action)
# --- 3. HELP MENU ---
help_menu = menu_bar.addMenu("Help")
help_instructions_action = QAction("User Guide & Instructions", self)
help_instructions_action.setMenuRole(QAction.MenuRole.ApplicationSpecificRole)
help_instructions_action.setShortcut(QKeySequence.StandardKey.HelpContents)
help_instructions_action.triggered.connect(self.show_help_dialog)
help_menu.addAction(help_instructions_action)
# --- Menu Action Handlers ---
@pyqtSlot()
def show_about_dialog(self):
"""Displays application details and copyright information."""
QMessageBox.about(
self,
"About Spanish Voice Trainer",
"<h3>Spanish Voice Trainer</h3>"
"<p><b>Version:</b> 1.0.0</p>"
"<p>A local-first application for Castilian Spanish vocabulary practice, "
"neural TTS voice previewing, and Anki deck generation.</p>"
)
@pyqtSlot()
def check_for_updates(self):
"""Placeholder action for update checks."""
QMessageBox.information(
self,
"Check for Updates",
"Check for Updates is not implemented yet in this version."
)
@pyqtSlot()
def handle_backup_database(self):
"""Copies the primary SQLite database file to a user-selected location."""
db_path = database.DB_PATH if hasattr(database, 'DB_PATH') else "spanish_app.db"
if not os.path.exists(db_path):
QMessageBox.warning(self, "Backup Error", f"Database file '{db_path}' could not be located.")
return
save_path, _ = QFileDialog.getSaveFileName(
self,
"Save Database Backup",
os.path.expanduser("~/Desktop/spanish_app_backup.db"),
"SQLite Database (*.db *.sqlite);;All Files (*)"
)
if save_path:
try:
shutil.copy2(db_path, save_path)
QMessageBox.information(
self,
"Backup Complete",
f"Successfully created a backup copy of your database at:\n{save_path}"
)
except Exception as e:
QMessageBox.critical(self, "Backup Failed", f"Could not create database backup:\n{str(e)}")
@pyqtSlot()
def show_help_dialog(self):
"""Displays user guide and usage instructions."""
instructions_text = (
"<h3>Spanish Voice Trainer - Quick Start Guide</h3>"
"<ul>"
"<li><b>Database Sandbox:</b> Add, edit, sort, or tag your English/Spanish phrase translations. "
"Sort order dictates sequence in flashcards and exports.</li>"
"<li><b>Flashcard Review:</b> Practice interactive flashcards. Click <i>Flip Card</i> to see and automatically hear "
"the Spanish phrase. Click <i>Next Card</i> to advance and automatically hear the English prompt.</li>"
"<li><b>Filter Cards:</b> Use the top filter fields (Context or Tags) to isolate specific review topics or decks.</li>"
"<li><b>Generate Deck:</b> Click <i>Generate Deck</i> to compile your active phrases into a `.apkg` file ready for Anki import.</li>"
"<li><b>Configuration Settings:</b> Adjust default export folders, speech speed rate, and Anki deck hierarchy settings.</li>"
"</ul>"
)
QMessageBox.information(self, "Instructions & Help", instructions_text)
# --- Standard Application Signal & Cache Logic ---
def refresh_settings_cache(self):
"""Loads all database settings into an easy-to-read dictionary."""
self.app_settings = database.load_all_settings()
@ -97,7 +214,7 @@ class MainWindow(QMainWindow):
records = database.get_all_translations_explicit()
if not records:
QMessageBox.warning(self, "Generation Failed", "No phrases available to construct a training video playlist loops.")
QMessageBox.warning(self, "Generation Failed", "No phrases available to construct training video playlist loops.")
return
# UI updates can block execution loops, so we notify before processing
@ -121,6 +238,10 @@ class MainWindow(QMainWindow):
if __name__ == "__main__":
app = QApplication(sys.argv)
# Explicitly set application identity for macOS menu bar and dock integration
app.setApplicationName("Spanish Voice Trainer")
app.setApplicationDisplayName("Spanish Voice Trainer")
# Modern clean styling choices for desktop applications
app.setStyle('Fusion')

View file

@ -1,138 +0,0 @@
accelerate==1.14.0
aiohappyeyeballs==2.6.2
aiohttp==3.14.0
aiosignal==1.4.0
annotated-doc==0.0.4
annotated-types==0.7.0
antlr4-python3-runtime==4.9.3
anyio==4.13.0
attrs==26.1.0
audioop-lts==0.2.2
audioread==3.1.0
beautifulsoup4==4.15.0
cached-property==2.0.1
certifi==2026.5.20
cffi==2.0.0
charset-normalizer==3.4.7
chevron==0.14.0
click==8.4.1
colorlog==6.10.1
decorator==5.3.1
defusedxml==0.7.1
dill==0.4.1
docling==2.102.1
docling-core==2.82.0
docling-ibm-models==3.13.3
docling-parse==6.2.0
docling-slim==2.102.1
edge-tts==7.2.8
et-xmlfile==2.0.0
faker==40.23.0
fastdtw==0.3.4
filelock==3.29.3
filetype==1.2.0
frozendict==2.4.7
frozenlist==1.8.0
fsspec==2026.4.0
genanki==0.13.1
h11==0.16.0
hf-xet==1.5.1
httpcore==1.0.9
httpx==0.28.1
huggingface-hub==1.19.0
idna==3.18
jinja2==3.1.6
joblib==1.5.3
jsonlines==4.0.0
jsonref==1.1.0
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
latex2mathml==3.81.0
lazy-loader==0.5
librosa==0.11.0
llvmlite==0.47.0
lxml==6.1.1
mail-parser==4.4.0
markdown-it-py==4.2.0
marko==2.2.3
markupsafe==3.0.3
mdurl==0.1.2
mpire==2.10.2
mpmath==1.3.0
msgpack==1.1.2
multidict==6.7.1
multiprocess==0.70.19
narwhals==2.22.1
networkx==3.6.1
numba==0.65.1
numpy==2.4.6
omegaconf==2.3.1
opencv-python==4.13.0.92
openpyxl==3.1.5
packaging==26.2
pandas==3.0.3
pillow==12.2.0
platformdirs==4.10.0
pluggy==1.6.0
polyfactory==3.3.0
pooch==1.9.0
propcache==0.5.2
psutil==7.2.2
pyclipper==1.4.0
pycparser==3.0
pydantic==2.13.4
pydantic-core==2.46.4
pydantic-settings==2.14.1
pygments==2.20.0
pylatexenc==2.10
pypdfium2==5.9.0
pyqt6==6.11.0
pyqt6-qt6==6.11.1
pyqt6-sip==13.11.1
python-dateutil==2.9.0.post0
python-docx==1.2.0
python-dotenv==1.2.2
python-pptx==1.0.2
pyyaml==6.0.3
rapidocr==3.8.3
referencing==0.37.0
regex==2026.5.9
requests==2.34.2
rich==15.0.0
rpds-py==2026.5.1
rtree==1.4.1
safetensors==0.8.0
scikit-learn==1.9.0
scipy==1.17.1
semchunk==3.2.5
setuptools==81.0.0
shapely==2.1.2
shellingham==1.5.4
six==1.17.0
sounddevice==0.5.5
soundfile==0.14.0
soupsieve==2.8.4
soxr==1.1.0
standard-aifc==3.13.0
standard-chunk==3.13.0
standard-sunau==3.13.0
sympy==1.14.0
tabulate==0.10.0
threadpoolctl==3.6.0
tokenizers==0.22.2
torch==2.12.0
torchvision==0.27.0
tqdm==4.68.2
transformers==5.8.1
tree-sitter==0.25.2
tree-sitter-c==0.24.2
tree-sitter-javascript==0.25.0
tree-sitter-python==0.25.0
tree-sitter-typescript==0.23.2
typer==0.21.2
typing-extensions==4.15.0
typing-inspection==0.4.2
urllib3==2.7.0
websockets==16.0
xlsxwriter==3.2.9
yarl==1.24.2

View file

@ -4,7 +4,6 @@ import re
import subprocess
import tempfile
import threading
import asyncio
from datetime import datetime
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
@ -12,10 +11,10 @@ from PyQt6.QtWidgets import (
QHeaderView, QFormLayout, QMessageBox, QFrame, QStackedWidget
)
from PyQt6.QtCore import Qt, pyqtSlot
import edge_tts
import database
import anki_exporter
from tts_utils import parse_text_for_edgetts, get_configured_tts_rate
from tts_utils import parse_text_for_edgetts
from kokoro_utils import generate_local_speech
class ReviewTab(QWidget):
def __init__(self, parent=None):
@ -277,99 +276,82 @@ class ReviewTab(QWidget):
# Automatically play English neural audio on advancing to next card
self.play_card_audio()
def _async_edge_speech_worker(self, text, voice, rate_modifier):
"""Background thread worker to render neural speech with terminal debug logging."""
async def stream_audio():
temp_file = os.path.join(tempfile.gettempdir(), "review_card_audio.mp3")
# Clean up old file if present
if os.path.exists(temp_file):
try:
os.remove(temp_file)
except Exception:
pass
try:
print(f"[TTS Debug] Generating TTS -> Voice: {voice} | Rate: {rate_modifier} | Text: '{text}'")
communicate = edge_tts.Communicate(text=text, voice=voice, rate=rate_modifier)
await communicate.save(temp_file)
if os.path.exists(temp_file) and os.path.getsize(temp_file) > 0:
print(f"[TTS Debug] Audio ready ({os.path.getsize(temp_file)} bytes). Playing via afplay...")
result = subprocess.run(["afplay", temp_file], capture_output=True, text=True)
if result.returncode != 0:
print(f"[TTS Debug] afplay failed: {result.stderr}")
else:
print("[TTS Debug] Playback finished successfully.")
else:
print("[TTS Debug] Error: Audio file was not created or is 0 bytes.")
except Exception as e:
print(f"[TTS Debug] Exception during speech synthesis: {e}")
# Explicitly set up and run a clean event loop for this thread
def _local_kokoro_speech_worker(self, text, voice_name, speed_multiplier):
"""Background thread worker to render local neural speech via Kokoro without network calls."""
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(stream_audio())
loop.close()
print(f"[Kokoro Debug] Generating local TTS -> Voice: {voice_name} | Speed: {speed_multiplier} | Text: '{text}'")
temp_file = generate_local_speech(text, voice_name=voice_name, speed=speed_multiplier)
if os.path.exists(temp_file) and os.path.getsize(temp_file) > 0:
print(f"[Kokoro Debug] Audio ready. Playing via afplay...")
result = subprocess.run(["afplay", temp_file], capture_output=True, text=True)
if result.returncode != 0:
print(f"[Kokoro Debug] afplay failed: {result.stderr}")
else:
print("[Kokoro Debug] Playback finished successfully.")
else:
print("[Kokoro Debug] Error: Audio file was not created or is 0 bytes.")
except Exception as e:
print(f"[TTS Debug] Event loop error: {e}")
print(f"[Kokoro Debug] Exception during local speech synthesis: {e}")
@pyqtSlot()
def play_card_audio(self):
"""Auditions neural edge-tts voice based on active flashcard side."""
"""Triggers local Kokoro neural voice generation based on active flashcard side."""
if not (0 <= self.current_index < len(self.filtered_review_pool)):
return
record = self.filtered_review_pool[self.current_index]
settings = database.load_all_settings() or {}
rate_string = get_configured_tts_rate(settings)
# Load playback speed setting from database cache (default to 1.0)
try:
speed_multiplier = float(settings.get("tts_playback_speed", 1.0))
except (TypeError, ValueError):
speed_multiplier = 1.0
if not self.is_flipped:
raw_text = record.get("en_text", "")
spoken_text = parse_text_for_edgetts(raw_text)
voice = "en-US-EmmaNeural"
#voice = "af_sarah" # English voice reference in Kokoro
#voice = "af_heart"
voice = "af_bella" # Default English voice for testing
else:
raw_text = record.get("es_text", "")
spoken_text = parse_text_for_edgetts(raw_text)
is_male = (record.get("gender") == "Male")
voice = "es-ES-AlvaroNeural" if is_male else "es-ES-ElviraNeural"
# Kokoro Spanish voices: 'ef_dora' (female) / 'em_alex' (male)
voice = "em_alex" if is_male else "ef_dora"
# Respect <meta sound-off> flags or empty entries
if not spoken_text.strip():
print("[TTS Debug] Skipped: Parsed text is empty or muted via sound-off tag.")
print("[Kokoro Debug] Skipped: Parsed text is empty.")
return
threading.Thread(
target=self._async_edge_speech_worker,
args=(spoken_text, voice, rate_string),
target=self._local_kokoro_speech_worker,
args=(spoken_text, voice, speed_multiplier),
daemon=True
).start()
@pyqtSlot()
def generate_deck_action(self):
"""Generates a specialized .apkg Anki deck matching active filter parameters,
prepending yyyy-mm-dd-hhmm timestamp and matching Sub-Deck-Namespace-Hierarchy without brackets/parentheses."""
prepressing yyyy-mm-dd-hhmm timestamp and matching Sub-Deck-Namespace-Hierarchy without brackets/parentheses."""
if not self.filtered_review_pool:
QMessageBox.warning(self, "Export Aborted", "The current matching review deck queue is empty. Cannot compile an empty deck.")
return
try:
# Load active settings dictionary directly from database configurations
settings = database.load_all_settings() or {}
# Extract configurations targeting exact database schema names found in settings
target_dir = settings.get("anki_export_directory")
root_deck_name = settings.get("anki_root_deck_name")
sub_deck_hierarchy = settings.get("anki_sub_deck_name")
# Fallback handling to verify directories exist safely
if not target_dir or not os.path.isdir(str(target_dir)):
target_dir = os.path.expanduser("~/Desktop")
else:
target_dir = str(target_dir)
# --- Compile Full Namespace Tree Path ---
deck_tree_parts = []
if root_deck_name and str(root_deck_name).strip():
@ -381,23 +363,17 @@ class ReviewTab(QWidget):
if not deck_tree_parts:
deck_tree_parts.append("DefaultDeck")
# Join parts using Anki double-colon syntax (::) for internal Anki hierarchy
full_deck_namespace = "::".join(deck_tree_parts)
# --- Format Timestamp and Clean Filename ---
# Format: YYYY-MM-DD-HHMM
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M")
# Explicitly strip out parentheses/brackets before regex normalization
clean_namespace = full_deck_namespace.replace('(', '').replace(')', '').replace('[', '').replace(']', '')
clean_namespace = re.sub(r'[^a-zA-Z0-9]', '-', clean_namespace)
clean_namespace = re.sub(r'-+', '-', clean_namespace).strip('-')
# Complete output filename pattern: yyyy-mm-dd-hhmm-Sub-Deck-Namespace-Hierarchy.apkg
filename = f"{timestamp}-{clean_namespace}.apkg"
file_path = os.path.join(target_dir, filename)
# Execute actual compilation algorithm pipeline mapping filtered records cleanly
anki_exporter.compile_anki_package(self.filtered_review_pool, file_path, full_deck_namespace)
QMessageBox.information(

View file

@ -3,7 +3,6 @@ import subprocess
import tempfile
import os
import threading
import asyncio
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
@ -11,9 +10,9 @@ from PyQt6.QtWidgets import (
QRadioButton, QButtonGroup, QSpinBox
)
from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt
import edge_tts
import database
from tts_utils import parse_text_for_edgetts, get_configured_tts_rate
from tts_utils import parse_text_for_edgetts
from kokoro_utils import generate_local_speech
class TextEditorDialog(QDialog):
@ -120,7 +119,7 @@ class SandboxTab(QWidget):
self.txt_tags.setStyleSheet("padding: 6px; font-size: 14px;")
self.txt_tags.textChanged.connect(self.apply_live_grid_filter)
# --- Sort Order Field (Added after Tags) ---
# --- Sort Order Field ---
self.spn_sort_order = QSpinBox()
self.spn_sort_order.setRange(0, 999999)
self.spn_sort_order.setValue(0)
@ -367,19 +366,20 @@ class SandboxTab(QWidget):
self.table.setItem(visible_row_idx, 6, QTableWidgetItem(r.get("gender", "Female")))
visible_row_idx += 1
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}")
asyncio.run(stream_audio())
def _local_kokoro_speech_worker(self, text, voice_name, speed_multiplier):
"""Background thread worker to render local neural speech via Kokoro without network calls."""
try:
print(f"[Kokoro Debug] Sandbox Audition -> Voice: {voice_name} | Speed: {speed_multiplier} | Text: '{text}'")
temp_file = generate_local_speech(text, voice_name=voice_name, speed=speed_multiplier)
if os.path.exists(temp_file) and os.path.getsize(temp_file) > 0:
result = subprocess.run(["afplay", temp_file], capture_output=True, text=True)
if result.returncode != 0:
print(f"[Kokoro Debug] afplay failed: {result.stderr}")
else:
print("[Kokoro Debug] Error: Audio file was not created or is 0 bytes.")
except Exception as e:
print(f"Sandbox Audition Error: {e}")
@pyqtSlot()
def audition_english(self):
@ -390,11 +390,14 @@ class SandboxTab(QWidget):
return
settings = database.load_all_settings() or {}
rate_string = get_configured_tts_rate(settings)
try:
speed_multiplier = float(settings.get("tts_playback_speed", 1.0))
except (TypeError, ValueError):
speed_multiplier = 1.0
threading.Thread(
target=self._async_edge_speech_worker,
args=(spoken_text, "en-US-EmmaNeural", rate_string),
target=self._local_kokoro_speech_worker,
args=(spoken_text, "af_bella", speed_multiplier),
daemon=True
).start()
@ -406,12 +409,15 @@ class SandboxTab(QWidget):
if not spoken_text.strip():
return
voice = "es-ES-AlvaroNeural" if self.rb_male.isChecked() else "es-ES-ElviraNeural"
voice = "em_alex" if self.rb_male.isChecked() else "ef_dora"
settings = database.load_all_settings() or {}
rate_string = get_configured_tts_rate(settings)
try:
speed_multiplier = float(settings.get("tts_playback_speed", 1.0))
except (TypeError, ValueError):
speed_multiplier = 1.0
threading.Thread(
target=self._async_edge_speech_worker,
args=(spoken_text, voice, rate_string),
target=self._local_kokoro_speech_worker,
args=(spoken_text, voice, speed_multiplier),
daemon=True
).start()

View file

@ -0,0 +1,29 @@
# test_voices.py
import soundfile as sf
from kokoro_onnx import Kokoro
kokoro = Kokoro("kokoro-v1.0.onnx", "voices-v1.0.bin")
# Test phrases
#english_text = "Welcome to the Spanish voice trainer. Let's practice key phrases together."
#spanish_text = "Bienvenido al entrenador de voz en español. Practiquemos frases clave juntos."
english_text = "surname"
spanish_text = "apellido"
# Voices to test (English and Spanish presets)
voices_to_test = [
("af_bella", "en-us", english_text),
("af_nicole", "en-us", english_text),
("af_sky", "en-us", english_text),
("ef_dora", "es", spanish_text),
("em_alex", "es", spanish_text),
]
for voice, lang, text in voices_to_test:
print(f"Generating sample for {voice} ({lang})...")
samples, sample_rate = kokoro.create(text, voice=voice, speed=1.0, lang=lang)
filename = f"sample_{voice}.wav"
sf.write(filename, samples, sample_rate)
print("Done! You can play sample_af_bella.wav, sample_ef_dora.wav, etc. using afplay.")

981
tree.txt
View file

@ -1,981 +0,0 @@
.
├── LICENSE
├── README.md
├── README.pdf
├── Santiago_cathedral_2021_Sunset.jpg
├── __pycache__
│   ├── anki_exporter.cpython-313.pyc
│   ├── database.cpython-313.pyc
│   ├── tts_utils.cpython-313.pyc
│   └── video_generator.cpython-313.pyc
├── anki_exporter.py
├── app.icns
├── app_icon.iconset
│   ├── icon_128x128.png
│   ├── icon_128x128@2x.png
│   ├── icon_16x16.png
│   ├── icon_16x16@2x.png
│   ├── icon_256x256.png
│   ├── icon_256x256@2x.png
│   ├── icon_32x32.png
│   ├── icon_32x32@2x.png
│   ├── icon_512x512.png
│   └── icon_512x512@2x.png
├── aula_int_plus_1_glos_en_alfa.pdf
├── base_icon.png
├── build
│   └── main
│   ├── Analysis-00.toc
│   ├── BUNDLE-00.toc
│   ├── COLLECT-00.toc
│   ├── EXE-00.toc
│   ├── PKG-00.toc
│   ├── PYZ-00.pyz
│   ├── PYZ-00.toc
│   ├── base_library.zip
│   ├── localpycs
│   │   ├── pyimod01_archive.pyc
│   │   ├── pyimod02_importers.pyc
│   │   ├── pyimod03_ctypes.pyc
│   │   └── struct.pyc
│   ├── main
│   ├── main.pkg
│   ├── warn-main.txt
│   └── xref-main.html
├── core
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-313.pyc
│   │   ├── asset_generator.cpython-313.pyc
│   │   ├── bulk_importer.cpython-313.pyc
│   │   ├── clean_glossary.cpython-313.pyc
│   │   └── phrase_manager.cpython-313.pyc
│   ├── asset_generator.py
│   ├── audio_engine.py
│   ├── bulk_importer.py
│   ├── clean_glossary.py
│   └── phrase_manager.py
├── database.py
├── database_legacy
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-313.pyc
│   │   └── connection.cpython-313.pyc
│   ├── connection.py
│   ├── trainer_backup_20260618_220043.db
│   └── trainer_backup_20260618_224811.db
├── dist
│   ├── main
│   │   ├── _internal
│   │   │   ├── 81d243bd2c585b0f4821__mypyc.cpython-313-darwin.so
│   │   │   ├── PIL
│   │   │   │   ├── _avif.cpython-313-darwin.so
│   │   │   │   ├── _imaging.cpython-313-darwin.so
│   │   │   │   ├── _imagingcms.cpython-313-darwin.so
│   │   │   │   ├── _imagingft.cpython-313-darwin.so
│   │   │   │   ├── _imagingmath.cpython-313-darwin.so
│   │   │   │   ├── _imagingtk.cpython-313-darwin.so
│   │   │   │   └── _webp.cpython-313-darwin.so
│   │   │   ├── PyQt6
│   │   │   │   ├── Qt6
│   │   │   │   │   ├── lib
│   │   │   │   │   │   ├── QtCore.framework
│   │   │   │   │   │   │   ├── QtCore -> Versions/Current/QtCore
│   │   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   │   └── Versions
│   │   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   │   ├── QtCore
│   │   │   │   │   │   │   │   └── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── Current -> A
│   │   │   │   │   │   ├── QtDBus.framework
│   │   │   │   │   │   │   ├── QtDBus -> Versions/Current/QtDBus
│   │   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   │   └── Versions
│   │   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   │   ├── QtDBus
│   │   │   │   │   │   │   │   └── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── Current -> A
│   │   │   │   │   │   ├── QtGui.framework
│   │   │   │   │   │   │   ├── QtGui -> Versions/Current/QtGui
│   │   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   │   └── Versions
│   │   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   │   ├── QtGui
│   │   │   │   │   │   │   │   └── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── Current -> A
│   │   │   │   │   │   ├── QtNetwork.framework
│   │   │   │   │   │   │   ├── QtNetwork -> Versions/Current/QtNetwork
│   │   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   │   └── Versions
│   │   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   │   ├── QtNetwork
│   │   │   │   │   │   │   │   └── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── Current -> A
│   │   │   │   │   │   ├── QtPdf.framework
│   │   │   │   │   │   │   ├── QtPdf -> Versions/Current/QtPdf
│   │   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   │   └── Versions
│   │   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   │   ├── QtPdf
│   │   │   │   │   │   │   │   └── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── Current -> A
│   │   │   │   │   │   ├── QtSvg.framework
│   │   │   │   │   │   │   ├── QtSvg -> Versions/Current/QtSvg
│   │   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   │   └── Versions
│   │   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   │   ├── QtSvg
│   │   │   │   │   │   │   │   └── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── Current -> A
│   │   │   │   │   │   └── QtWidgets.framework
│   │   │   │   │   │   ├── QtWidgets -> Versions/Current/QtWidgets
│   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   └── Versions
│   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   ├── QtWidgets
│   │   │   │   │   │   │   └── Resources
│   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   └── Current -> A
│   │   │   │   │   ├── plugins
│   │   │   │   │   │   ├── generic
│   │   │   │   │   │   │   └── libqtuiotouchplugin.dylib
│   │   │   │   │   │   ├── iconengines
│   │   │   │   │   │   │   └── libqsvgicon.dylib
│   │   │   │   │   │   ├── imageformats
│   │   │   │   │   │   │   ├── libqgif.dylib
│   │   │   │   │   │   │   ├── libqicns.dylib
│   │   │   │   │   │   │   ├── libqico.dylib
│   │   │   │   │   │   │   ├── libqjpeg.dylib
│   │   │   │   │   │   │   ├── libqmacheif.dylib
│   │   │   │   │   │   │   ├── libqmacjp2.dylib
│   │   │   │   │   │   │   ├── libqpdf.dylib
│   │   │   │   │   │   │   ├── libqsvg.dylib
│   │   │   │   │   │   │   ├── libqtga.dylib
│   │   │   │   │   │   │   ├── libqtiff.dylib
│   │   │   │   │   │   │   ├── libqwbmp.dylib
│   │   │   │   │   │   │   └── libqwebp.dylib
│   │   │   │   │   │   ├── platforms
│   │   │   │   │   │   │   ├── libqcocoa.dylib
│   │   │   │   │   │   │   ├── libqminimal.dylib
│   │   │   │   │   │   │   └── libqoffscreen.dylib
│   │   │   │   │   │   └── styles
│   │   │   │   │   │   └── libqmacstyle.dylib
│   │   │   │   │   └── translations
│   │   │   │   │   ├── qt_ar.qm
│   │   │   │   │   ├── qt_bg.qm
│   │   │   │   │   ├── qt_ca.qm
│   │   │   │   │   ├── qt_cs.qm
│   │   │   │   │   ├── qt_da.qm
│   │   │   │   │   ├── qt_de.qm
│   │   │   │   │   ├── qt_en.qm
│   │   │   │   │   ├── qt_es.qm
│   │   │   │   │   ├── qt_fa.qm
│   │   │   │   │   ├── qt_fi.qm
│   │   │   │   │   ├── qt_fr.qm
│   │   │   │   │   ├── qt_gd.qm
│   │   │   │   │   ├── qt_gl.qm
│   │   │   │   │   ├── qt_he.qm
│   │   │   │   │   ├── qt_help_ar.qm
│   │   │   │   │   ├── qt_help_bg.qm
│   │   │   │   │   ├── qt_help_ca.qm
│   │   │   │   │   ├── qt_help_cs.qm
│   │   │   │   │   ├── qt_help_da.qm
│   │   │   │   │   ├── qt_help_de.qm
│   │   │   │   │   ├── qt_help_en.qm
│   │   │   │   │   ├── qt_help_es.qm
│   │   │   │   │   ├── qt_help_fr.qm
│   │   │   │   │   ├── qt_help_gl.qm
│   │   │   │   │   ├── qt_help_hr.qm
│   │   │   │   │   ├── qt_help_hu.qm
│   │   │   │   │   ├── qt_help_it.qm
│   │   │   │   │   ├── qt_help_ja.qm
│   │   │   │   │   ├── qt_help_ka.qm
│   │   │   │   │   ├── qt_help_ko.qm
│   │   │   │   │   ├── qt_help_nl.qm
│   │   │   │   │   ├── qt_help_nn.qm
│   │   │   │   │   ├── qt_help_pl.qm
│   │   │   │   │   ├── qt_help_pt_BR.qm
│   │   │   │   │   ├── qt_help_ru.qm
│   │   │   │   │   ├── qt_help_sk.qm
│   │   │   │   │   ├── qt_help_sl.qm
│   │   │   │   │   ├── qt_help_sv.qm
│   │   │   │   │   ├── qt_help_tr.qm
│   │   │   │   │   ├── qt_help_uk.qm
│   │   │   │   │   ├── qt_help_zh_CN.qm
│   │   │   │   │   ├── qt_help_zh_TW.qm
│   │   │   │   │   ├── qt_hr.qm
│   │   │   │   │   ├── qt_hu.qm
│   │   │   │   │   ├── qt_it.qm
│   │   │   │   │   ├── qt_ja.qm
│   │   │   │   │   ├── qt_ka.qm
│   │   │   │   │   ├── qt_ko.qm
│   │   │   │   │   ├── qt_lg.qm
│   │   │   │   │   ├── qt_lt.qm
│   │   │   │   │   ├── qt_lv.qm
│   │   │   │   │   ├── qt_nl.qm
│   │   │   │   │   ├── qt_nn.qm
│   │   │   │   │   ├── qt_pl.qm
│   │   │   │   │   ├── qt_pt_BR.qm
│   │   │   │   │   ├── qt_pt_PT.qm
│   │   │   │   │   ├── qt_ru.qm
│   │   │   │   │   ├── qt_sk.qm
│   │   │   │   │   ├── qt_sl.qm
│   │   │   │   │   ├── qt_sv.qm
│   │   │   │   │   ├── qt_tr.qm
│   │   │   │   │   ├── qt_uk.qm
│   │   │   │   │   ├── qt_zh_CN.qm
│   │   │   │   │   ├── qt_zh_TW.qm
│   │   │   │   │   ├── qtbase_ar.qm
│   │   │   │   │   ├── qtbase_bg.qm
│   │   │   │   │   ├── qtbase_ca.qm
│   │   │   │   │   ├── qtbase_cs.qm
│   │   │   │   │   ├── qtbase_da.qm
│   │   │   │   │   ├── qtbase_de.qm
│   │   │   │   │   ├── qtbase_en.qm
│   │   │   │   │   ├── qtbase_es.qm
│   │   │   │   │   ├── qtbase_fa.qm
│   │   │   │   │   ├── qtbase_fi.qm
│   │   │   │   │   ├── qtbase_fr.qm
│   │   │   │   │   ├── qtbase_gd.qm
│   │   │   │   │   ├── qtbase_he.qm
│   │   │   │   │   ├── qtbase_hr.qm
│   │   │   │   │   ├── qtbase_hu.qm
│   │   │   │   │   ├── qtbase_it.qm
│   │   │   │   │   ├── qtbase_ja.qm
│   │   │   │   │   ├── qtbase_ka.qm
│   │   │   │   │   ├── qtbase_ko.qm
│   │   │   │   │   ├── qtbase_lg.qm
│   │   │   │   │   ├── qtbase_lv.qm
│   │   │   │   │   ├── qtbase_nl.qm
│   │   │   │   │   ├── qtbase_nn.qm
│   │   │   │   │   ├── qtbase_pl.qm
│   │   │   │   │   ├── qtbase_pt_BR.qm
│   │   │   │   │   ├── qtbase_ru.qm
│   │   │   │   │   ├── qtbase_sk.qm
│   │   │   │   │   ├── qtbase_sv.qm
│   │   │   │   │   ├── qtbase_tr.qm
│   │   │   │   │   ├── qtbase_uk.qm
│   │   │   │   │   ├── qtbase_zh_CN.qm
│   │   │   │   │   └── qtbase_zh_TW.qm
│   │   │   │   ├── QtCore.abi3.so
│   │   │   │   ├── QtDBus.abi3.so
│   │   │   │   ├── QtGui.abi3.so
│   │   │   │   ├── QtWidgets.abi3.so
│   │   │   │   └── sip.cpython-313-darwin.so
│   │   │   ├── QtCore -> PyQt6/Qt6/lib/QtCore.framework/Versions/A/QtCore
│   │   │   ├── QtDBus -> PyQt6/Qt6/lib/QtDBus.framework/Versions/A/QtDBus
│   │   │   ├── QtGui -> PyQt6/Qt6/lib/QtGui.framework/Versions/A/QtGui
│   │   │   ├── QtNetwork -> PyQt6/Qt6/lib/QtNetwork.framework/Versions/A/QtNetwork
│   │   │   ├── QtPdf -> PyQt6/Qt6/lib/QtPdf.framework/Versions/A/QtPdf
│   │   │   ├── QtSvg -> PyQt6/Qt6/lib/QtSvg.framework/Versions/A/QtSvg
│   │   │   ├── QtWidgets -> PyQt6/Qt6/lib/QtWidgets.framework/Versions/A/QtWidgets
│   │   │   ├── aiohttp
│   │   │   │   ├── _http_parser.cpython-313-darwin.so
│   │   │   │   ├── _http_writer.cpython-313-darwin.so
│   │   │   │   └── _websocket
│   │   │   │   ├── mask.cpython-313-darwin.so
│   │   │   │   └── reader_c.cpython-313-darwin.so
│   │   │   ├── attrs-26.1.0.dist-info
│   │   │   │   ├── INSTALLER
│   │   │   │   ├── METADATA
│   │   │   │   ├── RECORD
│   │   │   │   ├── REQUESTED
│   │   │   │   ├── WHEEL
│   │   │   │   └── licenses
│   │   │   │   └── LICENSE
│   │   │   ├── base_library.zip
│   │   │   ├── certifi
│   │   │   │   ├── cacert.pem
│   │   │   │   └── py.typed
│   │   │   ├── charset_normalizer
│   │   │   │   ├── cd.cpython-313-darwin.so
│   │   │   │   └── md.cpython-313-darwin.so
│   │   │   ├── frozenlist
│   │   │   │   └── _frozenlist.cpython-313-darwin.so
│   │   │   ├── libXau.6.dylib -> PIL/.dylibs/libXau.6.dylib
│   │   │   ├── libavif.16.4.1.dylib -> PIL/.dylibs/libavif.16.4.1.dylib
│   │   │   ├── libbrotlicommon.1.2.0.dylib -> PIL/.dylibs/libbrotlicommon.1.2.0.dylib
│   │   │   ├── libbrotlidec.1.2.0.dylib -> PIL/.dylibs/libbrotlidec.1.2.0.dylib
│   │   │   ├── libfreetype.6.dylib -> PIL/.dylibs/libfreetype.6.dylib
│   │   │   ├── libharfbuzz.0.dylib -> PIL/.dylibs/libharfbuzz.0.dylib
│   │   │   ├── libjpeg.62.4.0.dylib -> PIL/.dylibs/libjpeg.62.4.0.dylib
│   │   │   ├── liblcms2.2.dylib -> PIL/.dylibs/liblcms2.2.dylib
│   │   │   ├── liblzma.5.dylib -> PIL/.dylibs/liblzma.5.dylib
│   │   │   ├── libopenjp2.2.5.4.dylib -> PIL/.dylibs/libopenjp2.2.5.4.dylib
│   │   │   ├── libpng16.16.dylib -> PIL/.dylibs/libpng16.16.dylib
│   │   │   ├── libpython3.13.dylib
│   │   │   ├── libsharpyuv.0.dylib -> PIL/.dylibs/libsharpyuv.0.dylib
│   │   │   ├── libtiff.6.dylib -> PIL/.dylibs/libtiff.6.dylib
│   │   │   ├── libwebp.7.dylib -> PIL/.dylibs/libwebp.7.dylib
│   │   │   ├── libwebpdemux.2.dylib -> PIL/.dylibs/libwebpdemux.2.dylib
│   │   │   ├── libwebpmux.3.dylib -> PIL/.dylibs/libwebpmux.3.dylib
│   │   │   ├── libxcb.1.1.0.dylib -> PIL/.dylibs/libxcb.1.1.0.dylib
│   │   │   ├── libz.1.3.1.zlib-ng.dylib -> PIL/.dylibs/libz.1.3.1.zlib-ng.dylib
│   │   │   ├── lxml
│   │   │   │   ├── _elementpath.cpython-313-darwin.so
│   │   │   │   ├── builder.cpython-313-darwin.so
│   │   │   │   ├── etree.cpython-313-darwin.so
│   │   │   │   ├── html
│   │   │   │   │   ├── _difflib.cpython-313-darwin.so
│   │   │   │   │   └── diff.cpython-313-darwin.so
│   │   │   │   ├── isoschematron
│   │   │   │   │   └── resources
│   │   │   │   │   ├── rng
│   │   │   │   │   │   └── iso-schematron.rng
│   │   │   │   │   └── xsl
│   │   │   │   │   ├── RNG2Schtrn.xsl
│   │   │   │   │   ├── XSD2Schtrn.xsl
│   │   │   │   │   └── iso-schematron-xslt1
│   │   │   │   │   ├── iso_abstract_expand.xsl
│   │   │   │   │   ├── iso_dsdl_include.xsl
│   │   │   │   │   ├── iso_schematron_message.xsl
│   │   │   │   │   ├── iso_schematron_skeleton_for_xslt1.xsl
│   │   │   │   │   ├── iso_svrl_for_xslt1.xsl
│   │   │   │   │   └── readme.txt
│   │   │   │   ├── objectify.cpython-313-darwin.so
│   │   │   │   └── sax.cpython-313-darwin.so
│   │   │   ├── multidict
│   │   │   │   └── _multidict.cpython-313-darwin.so
│   │   │   ├── numpy
│   │   │   │   ├── _core
│   │   │   │   │   ├── _multiarray_tests.cpython-313-darwin.so
│   │   │   │   │   └── _multiarray_umath.cpython-313-darwin.so
│   │   │   │   ├── fft
│   │   │   │   │   └── _pocketfft_umath.cpython-313-darwin.so
│   │   │   │   ├── linalg
│   │   │   │   │   └── _umath_linalg.cpython-313-darwin.so
│   │   │   │   └── random
│   │   │   │   ├── _bounded_integers.cpython-313-darwin.so
│   │   │   │   ├── _common.cpython-313-darwin.so
│   │   │   │   ├── _generator.cpython-313-darwin.so
│   │   │   │   ├── _mt19937.cpython-313-darwin.so
│   │   │   │   ├── _pcg64.cpython-313-darwin.so
│   │   │   │   ├── _philox.cpython-313-darwin.so
│   │   │   │   ├── _sfc64.cpython-313-darwin.so
│   │   │   │   ├── bit_generator.cpython-313-darwin.so
│   │   │   │   └── mtrand.cpython-313-darwin.so
│   │   │   ├── numpy-2.4.6.dist-info
│   │   │   │   ├── INSTALLER
│   │   │   │   ├── METADATA
│   │   │   │   ├── RECORD
│   │   │   │   ├── REQUESTED
│   │   │   │   ├── WHEEL
│   │   │   │   ├── entry_points.txt
│   │   │   │   └── licenses
│   │   │   │   ├── LICENSE.txt
│   │   │   │   └── numpy
│   │   │   │   ├── _core
│   │   │   │   │   ├── include
│   │   │   │   │   │   └── numpy
│   │   │   │   │   │   └── libdivide
│   │   │   │   │   │   └── LICENSE.txt
│   │   │   │   │   └── src
│   │   │   │   │   ├── common
│   │   │   │   │   │   └── pythoncapi-compat
│   │   │   │   │   │   └── COPYING
│   │   │   │   │   ├── highway
│   │   │   │   │   │   └── LICENSE
│   │   │   │   │   ├── multiarray
│   │   │   │   │   │   └── dragon4_LICENSE.txt
│   │   │   │   │   ├── npysort
│   │   │   │   │   │   └── x86-simd-sort
│   │   │   │   │   │   └── LICENSE.md
│   │   │   │   │   └── umath
│   │   │   │   │   └── svml
│   │   │   │   │   └── LICENSE
│   │   │   │   ├── fft
│   │   │   │   │   └── pocketfft
│   │   │   │   │   └── LICENSE.md
│   │   │   │   ├── linalg
│   │   │   │   │   └── lapack_lite
│   │   │   │   │   └── LICENSE.txt
│   │   │   │   ├── ma
│   │   │   │   │   └── LICENSE
│   │   │   │   └── random
│   │   │   │   ├── LICENSE.md
│   │   │   │   └── src
│   │   │   │   ├── distributions
│   │   │   │   │   └── LICENSE.md
│   │   │   │   ├── mt19937
│   │   │   │   │   └── LICENSE.md
│   │   │   │   ├── pcg64
│   │   │   │   │   └── LICENSE.md
│   │   │   │   ├── philox
│   │   │   │   │   └── LICENSE.md
│   │   │   │   ├── sfc64
│   │   │   │   │   └── LICENSE.md
│   │   │   │   └── splitmix64
│   │   │   │   └── LICENSE.md
│   │   │   ├── propcache
│   │   │   │   └── _helpers_c.cpython-313-darwin.so
│   │   │   ├── psutil
│   │   │   │   └── _psutil_osx.abi3.so
│   │   │   ├── pydantic-2.13.4.dist-info
│   │   │   │   ├── INSTALLER
│   │   │   │   ├── METADATA
│   │   │   │   ├── RECORD
│   │   │   │   ├── REQUESTED
│   │   │   │   ├── WHEEL
│   │   │   │   └── licenses
│   │   │   │   └── LICENSE
│   │   │   ├── pydantic_core
│   │   │   │   └── _pydantic_core.cpython-313-darwin.so
│   │   │   ├── setuptools
│   │   │   │   └── _vendor
│   │   │   │   ├── importlib_metadata-8.7.1.dist-info
│   │   │   │   │   ├── INSTALLER
│   │   │   │   │   ├── METADATA
│   │   │   │   │   ├── RECORD
│   │   │   │   │   ├── REQUESTED
│   │   │   │   │   ├── WHEEL
│   │   │   │   │   ├── licenses
│   │   │   │   │   │   └── LICENSE
│   │   │   │   │   └── top_level.txt
│   │   │   │   └── jaraco
│   │   │   │   └── text
│   │   │   │   └── Lorem ipsum.txt
│   │   │   ├── yaml
│   │   │   │   └── _yaml.cpython-313-darwin.so
│   │   │   └── yarl
│   │   │   └── _quoting_c.cpython-313-darwin.so
│   │   └── main
│   └── main.app
│   └── Contents
│   ├── Frameworks
│   │   ├── 81d243bd2c585b0f4821__mypyc.cpython-313-darwin.so
│   │   ├── PIL
│   │   │   ├── __dot__dylibs
│   │   │   │   ├── libXau.6.dylib
│   │   │   │   ├── libavif.16.4.1.dylib
│   │   │   │   ├── libbrotlicommon.1.2.0.dylib
│   │   │   │   ├── libbrotlidec.1.2.0.dylib
│   │   │   │   ├── libfreetype.6.dylib
│   │   │   │   ├── libharfbuzz.0.dylib
│   │   │   │   ├── libjpeg.62.4.0.dylib
│   │   │   │   ├── liblcms2.2.dylib
│   │   │   │   ├── liblzma.5.dylib
│   │   │   │   ├── libopenjp2.2.5.4.dylib
│   │   │   │   ├── libpng16.16.dylib
│   │   │   │   ├── libsharpyuv.0.dylib
│   │   │   │   ├── libtiff.6.dylib
│   │   │   │   ├── libwebp.7.dylib
│   │   │   │   ├── libwebpdemux.2.dylib
│   │   │   │   ├── libwebpmux.3.dylib
│   │   │   │   ├── libxcb.1.1.0.dylib
│   │   │   │   └── libz.1.3.1.zlib-ng.dylib
│   │   │   ├── _avif.cpython-313-darwin.so
│   │   │   ├── _imaging.cpython-313-darwin.so
│   │   │   ├── _imagingcms.cpython-313-darwin.so
│   │   │   ├── _imagingft.cpython-313-darwin.so
│   │   │   ├── _imagingmath.cpython-313-darwin.so
│   │   │   ├── _imagingtk.cpython-313-darwin.so
│   │   │   └── _webp.cpython-313-darwin.so
│   │   ├── PyQt6
│   │   │   ├── Qt6
│   │   │   │   ├── lib
│   │   │   │   │   ├── QtCore.framework
│   │   │   │   │   │   ├── QtCore -> Versions/Current/QtCore
│   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   └── Versions
│   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   ├── QtCore
│   │   │   │   │   │   │   ├── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── _CodeSignature
│   │   │   │   │   │   │   └── CodeResources
│   │   │   │   │   │   └── Current -> A
│   │   │   │   │   ├── QtDBus.framework
│   │   │   │   │   │   ├── QtDBus -> Versions/Current/QtDBus
│   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   └── Versions
│   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   ├── QtDBus
│   │   │   │   │   │   │   ├── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── _CodeSignature
│   │   │   │   │   │   │   └── CodeResources
│   │   │   │   │   │   └── Current -> A
│   │   │   │   │   ├── QtGui.framework
│   │   │   │   │   │   ├── QtGui -> Versions/Current/QtGui
│   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   └── Versions
│   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   ├── QtGui
│   │   │   │   │   │   │   ├── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── _CodeSignature
│   │   │   │   │   │   │   └── CodeResources
│   │   │   │   │   │   └── Current -> A
│   │   │   │   │   ├── QtNetwork.framework
│   │   │   │   │   │   ├── QtNetwork -> Versions/Current/QtNetwork
│   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   └── Versions
│   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   ├── QtNetwork
│   │   │   │   │   │   │   ├── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── _CodeSignature
│   │   │   │   │   │   │   └── CodeResources
│   │   │   │   │   │   └── Current -> A
│   │   │   │   │   ├── QtPdf.framework
│   │   │   │   │   │   ├── QtPdf -> Versions/Current/QtPdf
│   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   └── Versions
│   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   ├── QtPdf
│   │   │   │   │   │   │   ├── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── _CodeSignature
│   │   │   │   │   │   │   └── CodeResources
│   │   │   │   │   │   └── Current -> A
│   │   │   │   │   ├── QtSvg.framework
│   │   │   │   │   │   ├── QtSvg -> Versions/Current/QtSvg
│   │   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   │   └── Versions
│   │   │   │   │   │   ├── A
│   │   │   │   │   │   │   ├── QtSvg
│   │   │   │   │   │   │   ├── Resources
│   │   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   │   └── _CodeSignature
│   │   │   │   │   │   │   └── CodeResources
│   │   │   │   │   │   └── Current -> A
│   │   │   │   │   └── QtWidgets.framework
│   │   │   │   │   ├── QtWidgets -> Versions/Current/QtWidgets
│   │   │   │   │   ├── Resources -> Versions/Current/Resources
│   │   │   │   │   └── Versions
│   │   │   │   │   ├── A
│   │   │   │   │   │   ├── QtWidgets
│   │   │   │   │   │   ├── Resources
│   │   │   │   │   │   │   └── Info.plist
│   │   │   │   │   │   └── _CodeSignature
│   │   │   │   │   │   └── CodeResources
│   │   │   │   │   └── Current -> A
│   │   │   │   ├── plugins
│   │   │   │   │   ├── generic
│   │   │   │   │   │   └── libqtuiotouchplugin.dylib
│   │   │   │   │   ├── iconengines
│   │   │   │   │   │   └── libqsvgicon.dylib
│   │   │   │   │   ├── imageformats
│   │   │   │   │   │   ├── libqgif.dylib
│   │   │   │   │   │   ├── libqicns.dylib
│   │   │   │   │   │   ├── libqico.dylib
│   │   │   │   │   │   ├── libqjpeg.dylib
│   │   │   │   │   │   ├── libqmacheif.dylib
│   │   │   │   │   │   ├── libqmacjp2.dylib
│   │   │   │   │   │   ├── libqpdf.dylib
│   │   │   │   │   │   ├── libqsvg.dylib
│   │   │   │   │   │   ├── libqtga.dylib
│   │   │   │   │   │   ├── libqtiff.dylib
│   │   │   │   │   │   ├── libqwbmp.dylib
│   │   │   │   │   │   └── libqwebp.dylib
│   │   │   │   │   ├── platforms
│   │   │   │   │   │   ├── libqcocoa.dylib
│   │   │   │   │   │   ├── libqminimal.dylib
│   │   │   │   │   │   └── libqoffscreen.dylib
│   │   │   │   │   └── styles
│   │   │   │   │   └── libqmacstyle.dylib
│   │   │   │   └── translations -> ../../../Resources/PyQt6/Qt6/translations
│   │   │   ├── QtCore.abi3.so
│   │   │   ├── QtDBus.abi3.so
│   │   │   ├── QtGui.abi3.so
│   │   │   ├── QtWidgets.abi3.so
│   │   │   └── sip.cpython-313-darwin.so
│   │   ├── QtCore -> PyQt6/Qt6/lib/QtCore.framework/Versions/A/QtCore
│   │   ├── QtDBus -> PyQt6/Qt6/lib/QtDBus.framework/Versions/A/QtDBus
│   │   ├── QtGui -> PyQt6/Qt6/lib/QtGui.framework/Versions/A/QtGui
│   │   ├── QtNetwork -> PyQt6/Qt6/lib/QtNetwork.framework/Versions/A/QtNetwork
│   │   ├── QtPdf -> PyQt6/Qt6/lib/QtPdf.framework/Versions/A/QtPdf
│   │   ├── QtSvg -> PyQt6/Qt6/lib/QtSvg.framework/Versions/A/QtSvg
│   │   ├── QtWidgets -> PyQt6/Qt6/lib/QtWidgets.framework/Versions/A/QtWidgets
│   │   ├── aiohttp
│   │   │   ├── _http_parser.cpython-313-darwin.so
│   │   │   ├── _http_writer.cpython-313-darwin.so
│   │   │   └── _websocket
│   │   │   ├── mask.cpython-313-darwin.so
│   │   │   └── reader_c.cpython-313-darwin.so
│   │   ├── attrs-26.1.0.dist-info -> ../Resources/attrs-26.1.0.dist-info
│   │   ├── base_library.zip -> ../Resources/base_library.zip
│   │   ├── certifi -> ../Resources/certifi
│   │   ├── charset_normalizer
│   │   │   ├── cd.cpython-313-darwin.so
│   │   │   └── md.cpython-313-darwin.so
│   │   ├── frozenlist
│   │   │   └── _frozenlist.cpython-313-darwin.so
│   │   ├── libXau.6.dylib -> PIL/.dylibs/libXau.6.dylib
│   │   ├── libavif.16.4.1.dylib -> PIL/.dylibs/libavif.16.4.1.dylib
│   │   ├── libbrotlicommon.1.2.0.dylib -> PIL/.dylibs/libbrotlicommon.1.2.0.dylib
│   │   ├── libbrotlidec.1.2.0.dylib -> PIL/.dylibs/libbrotlidec.1.2.0.dylib
│   │   ├── libfreetype.6.dylib -> PIL/.dylibs/libfreetype.6.dylib
│   │   ├── libharfbuzz.0.dylib -> PIL/.dylibs/libharfbuzz.0.dylib
│   │   ├── libjpeg.62.4.0.dylib -> PIL/.dylibs/libjpeg.62.4.0.dylib
│   │   ├── liblcms2.2.dylib -> PIL/.dylibs/liblcms2.2.dylib
│   │   ├── liblzma.5.dylib -> PIL/.dylibs/liblzma.5.dylib
│   │   ├── libopenjp2.2.5.4.dylib -> PIL/.dylibs/libopenjp2.2.5.4.dylib
│   │   ├── libpng16.16.dylib -> PIL/.dylibs/libpng16.16.dylib
│   │   ├── libpython3.13.dylib
│   │   ├── libsharpyuv.0.dylib -> PIL/.dylibs/libsharpyuv.0.dylib
│   │   ├── libtiff.6.dylib -> PIL/.dylibs/libtiff.6.dylib
│   │   ├── libwebp.7.dylib -> PIL/.dylibs/libwebp.7.dylib
│   │   ├── libwebpdemux.2.dylib -> PIL/.dylibs/libwebpdemux.2.dylib
│   │   ├── libwebpmux.3.dylib -> PIL/.dylibs/libwebpmux.3.dylib
│   │   ├── libxcb.1.1.0.dylib -> PIL/.dylibs/libxcb.1.1.0.dylib
│   │   ├── libz.1.3.1.zlib-ng.dylib -> PIL/.dylibs/libz.1.3.1.zlib-ng.dylib
│   │   ├── lxml
│   │   │   ├── _elementpath.cpython-313-darwin.so
│   │   │   ├── builder.cpython-313-darwin.so
│   │   │   ├── etree.cpython-313-darwin.so
│   │   │   ├── html
│   │   │   │   ├── _difflib.cpython-313-darwin.so
│   │   │   │   └── diff.cpython-313-darwin.so
│   │   │   ├── isoschematron -> ../../Resources/lxml/isoschematron
│   │   │   ├── objectify.cpython-313-darwin.so
│   │   │   └── sax.cpython-313-darwin.so
│   │   ├── multidict
│   │   │   └── _multidict.cpython-313-darwin.so
│   │   ├── numpy
│   │   │   ├── _core
│   │   │   │   ├── _multiarray_tests.cpython-313-darwin.so
│   │   │   │   └── _multiarray_umath.cpython-313-darwin.so
│   │   │   ├── fft
│   │   │   │   └── _pocketfft_umath.cpython-313-darwin.so
│   │   │   ├── linalg
│   │   │   │   └── _umath_linalg.cpython-313-darwin.so
│   │   │   └── random
│   │   │   ├── _bounded_integers.cpython-313-darwin.so
│   │   │   ├── _common.cpython-313-darwin.so
│   │   │   ├── _generator.cpython-313-darwin.so
│   │   │   ├── _mt19937.cpython-313-darwin.so
│   │   │   ├── _pcg64.cpython-313-darwin.so
│   │   │   ├── _philox.cpython-313-darwin.so
│   │   │   ├── _sfc64.cpython-313-darwin.so
│   │   │   ├── bit_generator.cpython-313-darwin.so
│   │   │   └── mtrand.cpython-313-darwin.so
│   │   ├── numpy-2.4.6.dist-info -> ../Resources/numpy-2.4.6.dist-info
│   │   ├── propcache
│   │   │   └── _helpers_c.cpython-313-darwin.so
│   │   ├── psutil
│   │   │   └── _psutil_osx.abi3.so
│   │   ├── pydantic-2.13.4.dist-info -> ../Resources/pydantic-2.13.4.dist-info
│   │   ├── pydantic_core
│   │   │   └── _pydantic_core.cpython-313-darwin.so
│   │   ├── setuptools -> ../Resources/setuptools
│   │   ├── yaml
│   │   │   └── _yaml.cpython-313-darwin.so
│   │   └── yarl
│   │   └── _quoting_c.cpython-313-darwin.so
│   ├── Info.plist
│   ├── MacOS
│   │   └── main
│   ├── Resources
│   │   ├── 81d243bd2c585b0f4821__mypyc.cpython-313-darwin.so -> ../Frameworks/81d243bd2c585b0f4821__mypyc.cpython-313-darwin.so
│   │   ├── PIL -> ../Frameworks/PIL
│   │   ├── PyQt6
│   │   │   ├── Qt6
│   │   │   │   ├── lib -> ../../../Frameworks/PyQt6/Qt6/lib
│   │   │   │   ├── plugins -> ../../../Frameworks/PyQt6/Qt6/plugins
│   │   │   │   └── translations
│   │   │   │   ├── qt_ar.qm
│   │   │   │   ├── qt_bg.qm
│   │   │   │   ├── qt_ca.qm
│   │   │   │   ├── qt_cs.qm
│   │   │   │   ├── qt_da.qm
│   │   │   │   ├── qt_de.qm
│   │   │   │   ├── qt_en.qm
│   │   │   │   ├── qt_es.qm
│   │   │   │   ├── qt_fa.qm
│   │   │   │   ├── qt_fi.qm
│   │   │   │   ├── qt_fr.qm
│   │   │   │   ├── qt_gd.qm
│   │   │   │   ├── qt_gl.qm
│   │   │   │   ├── qt_he.qm
│   │   │   │   ├── qt_help_ar.qm
│   │   │   │   ├── qt_help_bg.qm
│   │   │   │   ├── qt_help_ca.qm
│   │   │   │   ├── qt_help_cs.qm
│   │   │   │   ├── qt_help_da.qm
│   │   │   │   ├── qt_help_de.qm
│   │   │   │   ├── qt_help_en.qm
│   │   │   │   ├── qt_help_es.qm
│   │   │   │   ├── qt_help_fr.qm
│   │   │   │   ├── qt_help_gl.qm
│   │   │   │   ├── qt_help_hr.qm
│   │   │   │   ├── qt_help_hu.qm
│   │   │   │   ├── qt_help_it.qm
│   │   │   │   ├── qt_help_ja.qm
│   │   │   │   ├── qt_help_ka.qm
│   │   │   │   ├── qt_help_ko.qm
│   │   │   │   ├── qt_help_nl.qm
│   │   │   │   ├── qt_help_nn.qm
│   │   │   │   ├── qt_help_pl.qm
│   │   │   │   ├── qt_help_pt_BR.qm
│   │   │   │   ├── qt_help_ru.qm
│   │   │   │   ├── qt_help_sk.qm
│   │   │   │   ├── qt_help_sl.qm
│   │   │   │   ├── qt_help_sv.qm
│   │   │   │   ├── qt_help_tr.qm
│   │   │   │   ├── qt_help_uk.qm
│   │   │   │   ├── qt_help_zh_CN.qm
│   │   │   │   ├── qt_help_zh_TW.qm
│   │   │   │   ├── qt_hr.qm
│   │   │   │   ├── qt_hu.qm
│   │   │   │   ├── qt_it.qm
│   │   │   │   ├── qt_ja.qm
│   │   │   │   ├── qt_ka.qm
│   │   │   │   ├── qt_ko.qm
│   │   │   │   ├── qt_lg.qm
│   │   │   │   ├── qt_lt.qm
│   │   │   │   ├── qt_lv.qm
│   │   │   │   ├── qt_nl.qm
│   │   │   │   ├── qt_nn.qm
│   │   │   │   ├── qt_pl.qm
│   │   │   │   ├── qt_pt_BR.qm
│   │   │   │   ├── qt_pt_PT.qm
│   │   │   │   ├── qt_ru.qm
│   │   │   │   ├── qt_sk.qm
│   │   │   │   ├── qt_sl.qm
│   │   │   │   ├── qt_sv.qm
│   │   │   │   ├── qt_tr.qm
│   │   │   │   ├── qt_uk.qm
│   │   │   │   ├── qt_zh_CN.qm
│   │   │   │   ├── qt_zh_TW.qm
│   │   │   │   ├── qtbase_ar.qm
│   │   │   │   ├── qtbase_bg.qm
│   │   │   │   ├── qtbase_ca.qm
│   │   │   │   ├── qtbase_cs.qm
│   │   │   │   ├── qtbase_da.qm
│   │   │   │   ├── qtbase_de.qm
│   │   │   │   ├── qtbase_en.qm
│   │   │   │   ├── qtbase_es.qm
│   │   │   │   ├── qtbase_fa.qm
│   │   │   │   ├── qtbase_fi.qm
│   │   │   │   ├── qtbase_fr.qm
│   │   │   │   ├── qtbase_gd.qm
│   │   │   │   ├── qtbase_he.qm
│   │   │   │   ├── qtbase_hr.qm
│   │   │   │   ├── qtbase_hu.qm
│   │   │   │   ├── qtbase_it.qm
│   │   │   │   ├── qtbase_ja.qm
│   │   │   │   ├── qtbase_ka.qm
│   │   │   │   ├── qtbase_ko.qm
│   │   │   │   ├── qtbase_lg.qm
│   │   │   │   ├── qtbase_lv.qm
│   │   │   │   ├── qtbase_nl.qm
│   │   │   │   ├── qtbase_nn.qm
│   │   │   │   ├── qtbase_pl.qm
│   │   │   │   ├── qtbase_pt_BR.qm
│   │   │   │   ├── qtbase_ru.qm
│   │   │   │   ├── qtbase_sk.qm
│   │   │   │   ├── qtbase_sv.qm
│   │   │   │   ├── qtbase_tr.qm
│   │   │   │   ├── qtbase_uk.qm
│   │   │   │   ├── qtbase_zh_CN.qm
│   │   │   │   └── qtbase_zh_TW.qm
│   │   │   ├── QtCore.abi3.so -> ../../Frameworks/PyQt6/QtCore.abi3.so
│   │   │   ├── QtDBus.abi3.so -> ../../Frameworks/PyQt6/QtDBus.abi3.so
│   │   │   ├── QtGui.abi3.so -> ../../Frameworks/PyQt6/QtGui.abi3.so
│   │   │   ├── QtWidgets.abi3.so -> ../../Frameworks/PyQt6/QtWidgets.abi3.so
│   │   │   └── sip.cpython-313-darwin.so -> ../../Frameworks/PyQt6/sip.cpython-313-darwin.so
│   │   ├── QtCore -> PyQt6/Qt6/lib/QtCore.framework/Versions/A/QtCore
│   │   ├── QtDBus -> PyQt6/Qt6/lib/QtDBus.framework/Versions/A/QtDBus
│   │   ├── QtGui -> PyQt6/Qt6/lib/QtGui.framework/Versions/A/QtGui
│   │   ├── QtNetwork -> PyQt6/Qt6/lib/QtNetwork.framework/Versions/A/QtNetwork
│   │   ├── QtPdf -> PyQt6/Qt6/lib/QtPdf.framework/Versions/A/QtPdf
│   │   ├── QtSvg -> PyQt6/Qt6/lib/QtSvg.framework/Versions/A/QtSvg
│   │   ├── QtWidgets -> PyQt6/Qt6/lib/QtWidgets.framework/Versions/A/QtWidgets
│   │   ├── aiohttp -> ../Frameworks/aiohttp
│   │   ├── app.icns
│   │   ├── attrs-26.1.0.dist-info
│   │   │   ├── INSTALLER
│   │   │   ├── METADATA
│   │   │   ├── RECORD
│   │   │   ├── REQUESTED
│   │   │   ├── WHEEL
│   │   │   └── licenses
│   │   │   └── LICENSE
│   │   ├── base_library.zip
│   │   ├── certifi
│   │   │   ├── cacert.pem
│   │   │   └── py.typed
│   │   ├── charset_normalizer -> ../Frameworks/charset_normalizer
│   │   ├── frozenlist -> ../Frameworks/frozenlist
│   │   ├── libXau.6.dylib -> PIL/.dylibs/libXau.6.dylib
│   │   ├── libavif.16.4.1.dylib -> PIL/.dylibs/libavif.16.4.1.dylib
│   │   ├── libbrotlicommon.1.2.0.dylib -> PIL/.dylibs/libbrotlicommon.1.2.0.dylib
│   │   ├── libbrotlidec.1.2.0.dylib -> PIL/.dylibs/libbrotlidec.1.2.0.dylib
│   │   ├── libfreetype.6.dylib -> PIL/.dylibs/libfreetype.6.dylib
│   │   ├── libharfbuzz.0.dylib -> PIL/.dylibs/libharfbuzz.0.dylib
│   │   ├── libjpeg.62.4.0.dylib -> PIL/.dylibs/libjpeg.62.4.0.dylib
│   │   ├── liblcms2.2.dylib -> PIL/.dylibs/liblcms2.2.dylib
│   │   ├── liblzma.5.dylib -> PIL/.dylibs/liblzma.5.dylib
│   │   ├── libopenjp2.2.5.4.dylib -> PIL/.dylibs/libopenjp2.2.5.4.dylib
│   │   ├── libpng16.16.dylib -> PIL/.dylibs/libpng16.16.dylib
│   │   ├── libpython3.13.dylib -> ../Frameworks/libpython3.13.dylib
│   │   ├── libsharpyuv.0.dylib -> PIL/.dylibs/libsharpyuv.0.dylib
│   │   ├── libtiff.6.dylib -> PIL/.dylibs/libtiff.6.dylib
│   │   ├── libwebp.7.dylib -> PIL/.dylibs/libwebp.7.dylib
│   │   ├── libwebpdemux.2.dylib -> PIL/.dylibs/libwebpdemux.2.dylib
│   │   ├── libwebpmux.3.dylib -> PIL/.dylibs/libwebpmux.3.dylib
│   │   ├── libxcb.1.1.0.dylib -> PIL/.dylibs/libxcb.1.1.0.dylib
│   │   ├── libz.1.3.1.zlib-ng.dylib -> PIL/.dylibs/libz.1.3.1.zlib-ng.dylib
│   │   ├── lxml
│   │   │   ├── _elementpath.cpython-313-darwin.so -> ../../Frameworks/lxml/_elementpath.cpython-313-darwin.so
│   │   │   ├── builder.cpython-313-darwin.so -> ../../Frameworks/lxml/builder.cpython-313-darwin.so
│   │   │   ├── etree.cpython-313-darwin.so -> ../../Frameworks/lxml/etree.cpython-313-darwin.so
│   │   │   ├── html -> ../../Frameworks/lxml/html
│   │   │   ├── isoschematron
│   │   │   │   └── resources
│   │   │   │   ├── rng
│   │   │   │   │   └── iso-schematron.rng
│   │   │   │   └── xsl
│   │   │   │   ├── RNG2Schtrn.xsl
│   │   │   │   ├── XSD2Schtrn.xsl
│   │   │   │   └── iso-schematron-xslt1
│   │   │   │   ├── iso_abstract_expand.xsl
│   │   │   │   ├── iso_dsdl_include.xsl
│   │   │   │   ├── iso_schematron_message.xsl
│   │   │   │   ├── iso_schematron_skeleton_for_xslt1.xsl
│   │   │   │   ├── iso_svrl_for_xslt1.xsl
│   │   │   │   └── readme.txt
│   │   │   ├── objectify.cpython-313-darwin.so -> ../../Frameworks/lxml/objectify.cpython-313-darwin.so
│   │   │   └── sax.cpython-313-darwin.so -> ../../Frameworks/lxml/sax.cpython-313-darwin.so
│   │   ├── multidict -> ../Frameworks/multidict
│   │   ├── numpy -> ../Frameworks/numpy
│   │   ├── numpy-2.4.6.dist-info
│   │   │   ├── INSTALLER
│   │   │   ├── METADATA
│   │   │   ├── RECORD
│   │   │   ├── REQUESTED
│   │   │   ├── WHEEL
│   │   │   ├── entry_points.txt
│   │   │   └── licenses
│   │   │   ├── LICENSE.txt
│   │   │   └── numpy
│   │   │   ├── _core
│   │   │   │   ├── include
│   │   │   │   │   └── numpy
│   │   │   │   │   └── libdivide
│   │   │   │   │   └── LICENSE.txt
│   │   │   │   └── src
│   │   │   │   ├── common
│   │   │   │   │   └── pythoncapi-compat
│   │   │   │   │   └── COPYING
│   │   │   │   ├── highway
│   │   │   │   │   └── LICENSE
│   │   │   │   ├── multiarray
│   │   │   │   │   └── dragon4_LICENSE.txt
│   │   │   │   ├── npysort
│   │   │   │   │   └── x86-simd-sort
│   │   │   │   │   └── LICENSE.md
│   │   │   │   └── umath
│   │   │   │   └── svml
│   │   │   │   └── LICENSE
│   │   │   ├── fft
│   │   │   │   └── pocketfft
│   │   │   │   └── LICENSE.md
│   │   │   ├── linalg
│   │   │   │   └── lapack_lite
│   │   │   │   └── LICENSE.txt
│   │   │   ├── ma
│   │   │   │   └── LICENSE
│   │   │   └── random
│   │   │   ├── LICENSE.md
│   │   │   └── src
│   │   │   ├── distributions
│   │   │   │   └── LICENSE.md
│   │   │   ├── mt19937
│   │   │   │   └── LICENSE.md
│   │   │   ├── pcg64
│   │   │   │   └── LICENSE.md
│   │   │   ├── philox
│   │   │   │   └── LICENSE.md
│   │   │   ├── sfc64
│   │   │   │   └── LICENSE.md
│   │   │   └── splitmix64
│   │   │   └── LICENSE.md
│   │   ├── propcache -> ../Frameworks/propcache
│   │   ├── psutil -> ../Frameworks/psutil
│   │   ├── pydantic-2.13.4.dist-info
│   │   │   ├── INSTALLER
│   │   │   ├── METADATA
│   │   │   ├── RECORD
│   │   │   ├── REQUESTED
│   │   │   ├── WHEEL
│   │   │   └── licenses
│   │   │   └── LICENSE
│   │   ├── pydantic_core -> ../Frameworks/pydantic_core
│   │   ├── setuptools
│   │   │   └── _vendor
│   │   │   ├── importlib_metadata-8.7.1.dist-info
│   │   │   │   ├── INSTALLER
│   │   │   │   ├── METADATA
│   │   │   │   ├── RECORD
│   │   │   │   ├── REQUESTED
│   │   │   │   ├── WHEEL
│   │   │   │   ├── licenses
│   │   │   │   │   └── LICENSE
│   │   │   │   └── top_level.txt
│   │   │   └── jaraco
│   │   │   └── text
│   │   │   └── Lorem ipsum.txt
│   │   ├── yaml -> ../Frameworks/yaml
│   │   └── yarl -> ../Frameworks/yarl
│   └── _CodeSignature
│   └── CodeResources
├── doc
│   ├── Notes.md
│   ├── Notes.pdf
│   └── images
│   ├── image-01.png
│   ├── image-02.png
│   └── image-03.png
├── docling_output.md
├── exported_phases.csv
├── main.py
├── main.spec
├── media
├── migrate_database.py
├── pyproject.toml
├── requirements.txt
├── spanish_alphabetical.txt
├── spanish_glossary_sorted.txt
├── spanish_glossary_sorted.xlsx
├── spanish_glossary_sorted_B.pdf
├── spanish_glossary_sorted_B.txt
├── spanish_glossary_sorted_B.xlsx
├── spanish_trainer-26-08-21.db
├── spanish_trainer-backup.db
├── spanish_trainer.db
├── spanish_trainer_backup.sql
├── spanish_trainer_legacy.db
├── tabs
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-313.pyc
│   │   ├── review_tab.cpython-313.pyc
│   │   ├── sandbox_tab.cpython-313.pyc
│   │   └── settings_tab.cpython-313.pyc
│   ├── review_tab.py
│   ├── sandbox_tab.py
│   └── settings_tab.py
├── test_dual.py
├── test_tts.py
├── tree.txt
├── tts_utils.py
├── ui
│   ├── __init__.py
│   ├── cli
│   │   └── interface.py
│   └── gui
│   ├── create_mode.py
│   ├── interface.py
│   └── trainer_mode.py
├── uv.lock
└── video_generator.py
289 directories, 690 files

View file

@ -3,24 +3,19 @@ import re
from bs4 import BeautifulSoup
def get_configured_tts_rate(settings: dict) -> str:
"""Extracts speed multiplier from settings dict and converts to Edge TTS percentage string (e.g., '-25%')."""
# Query the exact key name shown in SQLite database: 'tts_playback_speed'
def get_configured_tts_rate(settings: dict) -> float:
"""Extracts speed multiplier from settings dict as a clean float (e.g., 0.75) for Kokoro TTS."""
raw_val = settings.get("tts_playback_speed", 1.0) if settings else 1.0
try:
raw_rate = float(raw_val)
speed_rate = float(raw_val)
except (TypeError, ValueError):
raw_rate = 1.0
# Calculate percentage shift relative to baseline 1.0 (e.g., 0.75 -> -25%)
pct = int(round((raw_rate - 1.0) * 100))
rate_str = f"+{pct}%" if pct >= 0 else f"{pct}%"
speed_rate = 1.0
print(
f"[TTS Debug] DB Key 'tts_playback_speed': {raw_val} -> Formatted Rate: {rate_str}"
f"[TTS Debug] DB Key 'tts_playback_speed': {raw_val} -> Kokoro Speed Multiplier: {speed_rate}"
)
return rate_str
return speed_rate
def parse_text_for_edgetts(html_content: str) -> str: