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

" + "" + ) + 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: