diff --git a/.gitignore b/.gitignore
index c3a1c81..8f06c6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,7 @@ media/*
# IDE and Environment Configurations
.vscode/
.idea/
-.env
\ No newline at end of file
+.env
+
+*.onnx
+*.bin
\ No newline at end of file
diff --git a/Spanish Voice Trainer.spec b/Spanish Voice Trainer.spec
new file mode 100644
index 0000000..577af74
--- /dev/null
+++ b/Spanish Voice Trainer.spec
@@ -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,
+)
diff --git a/anki_exporter.py b/anki_exporter.py
index 1b2c3ef..614e1c8 100644
--- a/anki_exporter.py
+++ b/anki_exporter.py
@@ -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': '
TRANSLATE TO SPANISH:
'
- '{{EnglishText}}
'
- '{{EnglishAudio}}
',
+ '{{EnglishText}}
'
+ '{{EnglishAudio}}
',
'afmt': '{{FrontSide}}
'
'{{SpanishText}}
'
'{{#AnkiNotes}}Anki Meta: {{{AnkiNotes}}}
{{/AnkiNotes}}
'
@@ -62,8 +53,8 @@ def compile_anki_package(records, output_path, deck_name):
{
'name': 'Card 2: Spanish ➔ English',
'qfmt': 'TRANSLATE TO ENGLISH:
'
- '{{SpanishText}}
'
- '{{SpanishAudio}}
',
+ '{{SpanishText}}
'
+ '{{SpanishAudio}}
',
'afmt': '{{FrontSide}}
'
'{{EnglishText}}
'
'{{#AnkiNotes}}Anki Meta: {{{AnkiNotes}}}
{{/AnkiNotes}}
'
@@ -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)
\ No newline at end of file
+ package.write_to_file(output_path)
+
\ No newline at end of file
diff --git a/audio-samples/sample_af_bella.wav b/audio-samples/sample_af_bella.wav
new file mode 100644
index 0000000..abebe27
Binary files /dev/null and b/audio-samples/sample_af_bella.wav differ
diff --git a/audio-samples/sample_af_nicole.wav b/audio-samples/sample_af_nicole.wav
new file mode 100644
index 0000000..f850f69
Binary files /dev/null and b/audio-samples/sample_af_nicole.wav differ
diff --git a/audio-samples/sample_af_sky.wav b/audio-samples/sample_af_sky.wav
new file mode 100644
index 0000000..4178f77
Binary files /dev/null and b/audio-samples/sample_af_sky.wav differ
diff --git a/audio-samples/sample_ef_dora.wav b/audio-samples/sample_ef_dora.wav
new file mode 100644
index 0000000..17da0d4
Binary files /dev/null and b/audio-samples/sample_ef_dora.wav differ
diff --git a/audio-samples/sample_em_alex.wav b/audio-samples/sample_em_alex.wav
new file mode 100644
index 0000000..76c91e4
Binary files /dev/null and b/audio-samples/sample_em_alex.wav differ
diff --git a/Santiago_cathedral_2021_Sunset.jpg b/base-icons/Santiago_cathedral_2021_Sunset.jpg
similarity index 100%
rename from Santiago_cathedral_2021_Sunset.jpg
rename to base-icons/Santiago_cathedral_2021_Sunset.jpg
diff --git a/base_icon.png b/base-icons/base_icon.png
similarity index 100%
rename from base_icon.png
rename to base-icons/base_icon.png
diff --git a/migrate_database.py b/database_legacy/migrate_database.py
similarity index 100%
rename from migrate_database.py
rename to database_legacy/migrate_database.py
diff --git a/spanish_trainer-26-06-21.db b/database_legacy/spanish_trainer-26-06-21.db
similarity index 100%
rename from spanish_trainer-26-06-21.db
rename to database_legacy/spanish_trainer-26-06-21.db
diff --git a/spanish_trainer-26-08-21.db b/database_legacy/spanish_trainer-26-08-21.db
similarity index 100%
rename from spanish_trainer-26-08-21.db
rename to database_legacy/spanish_trainer-26-08-21.db
diff --git a/spanish_trainer-backup.db b/database_legacy/spanish_trainer-backup.db
similarity index 100%
rename from spanish_trainer-backup.db
rename to database_legacy/spanish_trainer-backup.db
diff --git a/spanish_trainer.db b/database_legacy/spanish_trainer.db
similarity index 100%
rename from spanish_trainer.db
rename to database_legacy/spanish_trainer.db
diff --git a/spanish_trainer_backup.sql b/database_legacy/spanish_trainer_backup.sql
similarity index 100%
rename from spanish_trainer_backup.sql
rename to database_legacy/spanish_trainer_backup.sql
diff --git a/spanish_trainer_legacy.db b/database_legacy/spanish_trainer_legacy.db
similarity index 100%
rename from spanish_trainer_legacy.db
rename to database_legacy/spanish_trainer_legacy.db
diff --git a/doc/Notes.md b/doc/Notes.md
index 579baf5..3d68da3 100644
--- a/doc/Notes.md
+++ b/doc/Notes.md
@@ -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`)
\ No newline at end of file
+* **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
diff --git a/doc/Notes.pdf b/doc/Notes.pdf
index 4dfe3e0..7010463 100644
Binary files a/doc/Notes.pdf and b/doc/Notes.pdf differ
diff --git a/aula_int_plus_1_glos_en_alfa.pdf b/doc/aula_int_plus_1_glos_en_alfa.pdf
similarity index 100%
rename from aula_int_plus_1_glos_en_alfa.pdf
rename to doc/aula_int_plus_1_glos_en_alfa.pdf
diff --git a/spanish_alphabetical.txt b/doc/spanish_alphabetical.txt
similarity index 100%
rename from spanish_alphabetical.txt
rename to doc/spanish_alphabetical.txt
diff --git a/spanish_glossary_sorted.txt b/doc/spanish_glossary_sorted.txt
similarity index 100%
rename from spanish_glossary_sorted.txt
rename to doc/spanish_glossary_sorted.txt
diff --git a/spanish_glossary_sorted.xlsx b/doc/spanish_glossary_sorted.xlsx
similarity index 100%
rename from spanish_glossary_sorted.xlsx
rename to doc/spanish_glossary_sorted.xlsx
diff --git a/spanish_glossary_sorted_B.pdf b/doc/spanish_glossary_sorted_B.pdf
similarity index 100%
rename from spanish_glossary_sorted_B.pdf
rename to doc/spanish_glossary_sorted_B.pdf
diff --git a/spanish_glossary_sorted_B.txt b/doc/spanish_glossary_sorted_B.txt
similarity index 100%
rename from spanish_glossary_sorted_B.txt
rename to doc/spanish_glossary_sorted_B.txt
diff --git a/spanish_glossary_sorted_B.xlsx b/doc/spanish_glossary_sorted_B.xlsx
similarity index 100%
rename from spanish_glossary_sorted_B.xlsx
rename to doc/spanish_glossary_sorted_B.xlsx
diff --git a/tree2.txt b/doc/tree2.txt
similarity index 100%
rename from tree2.txt
rename to doc/tree2.txt
diff --git a/kokoro_utils.py b/kokoro_utils.py
new file mode 100644
index 0000000..a59c870
--- /dev/null
+++ b/kokoro_utils.py
@@ -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
\ No newline at end of file
diff --git a/main.py b/main.py
index a473be3..6863805 100644
--- a/main.py
+++ b/main.py
@@ -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",
+ "Spanish Voice Trainer
"
+ "Version: 1.0.0
"
+ "A local-first application for Castilian Spanish vocabulary practice, "
+ "neural TTS voice previewing, and Anki deck generation.
"
+ )
+
+ @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 = (
+ "Spanish Voice Trainer - Quick Start Guide
"
+ ""
+ "- Database Sandbox: Add, edit, sort, or tag your English/Spanish phrase translations. "
+ "Sort order dictates sequence in flashcards and exports.
"
+ "- Flashcard Review: Practice interactive flashcards. Click Flip Card to see and automatically hear "
+ "the Spanish phrase. Click Next Card to advance and automatically hear the English prompt.
"
+ "- Filter Cards: Use the top filter fields (Context or Tags) to isolate specific review topics or decks.
"
+ "- Generate Deck: Click Generate Deck to compile your active phrases into a `.apkg` file ready for Anki import.
"
+ "- Configuration Settings: Adjust default export folders, speech speed rate, and Anki deck hierarchy settings.
"
+ "
"
+ )
+ 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')
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 94111a9..0000000
--- a/requirements.txt
+++ /dev/null
@@ -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
diff --git a/tabs/review_tab.py b/tabs/review_tab.py
index 208ff9f..a52d054 100644
--- a/tabs/review_tab.py
+++ b/tabs/review_tab.py
@@ -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 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(
diff --git a/tabs/sandbox_tab.py b/tabs/sandbox_tab.py
index 5809f7e..9ee00cc 100644
--- a/tabs/sandbox_tab.py
+++ b/tabs/sandbox_tab.py
@@ -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()
\ No newline at end of file
diff --git a/test_dual.py b/test-scripts/test_dual.py
similarity index 100%
rename from test_dual.py
rename to test-scripts/test_dual.py
diff --git a/test_tts.py b/test-scripts/test_tts.py
similarity index 100%
rename from test_tts.py
rename to test-scripts/test_tts.py
diff --git a/test-scripts/test_voices.py b/test-scripts/test_voices.py
new file mode 100644
index 0000000..d53bfce
--- /dev/null
+++ b/test-scripts/test_voices.py
@@ -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.")
\ No newline at end of file
diff --git a/tree.txt b/tree.txt
deleted file mode 100644
index 62b2776..0000000
--- a/tree.txt
+++ /dev/null
@@ -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
diff --git a/tts_utils.py b/tts_utils.py
index 71a34d3..75ec077 100644
--- a/tts_utils.py
+++ b/tts_utils.py
@@ -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: