diff --git a/.gitignore b/.gitignore
index 892341a..c3a1c81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,10 @@ __pycache__/
.venv/
.uv/
+dist/*
+build/*
+
+
# Local SQLite Databases
# *.db
# *.db-journal
diff --git a/Santiago_cathedral_2021_Sunset.jpg b/Santiago_cathedral_2021_Sunset.jpg
new file mode 100644
index 0000000..c1ba3f3
Binary files /dev/null and b/Santiago_cathedral_2021_Sunset.jpg differ
diff --git a/anki_exporter.py b/anki_exporter.py
index 9e301d2..96a13d7 100644
--- a/anki_exporter.py
+++ b/anki_exporter.py
@@ -2,9 +2,9 @@
import os
import tempfile
import asyncio
+import time
import genanki
import edge_tts
-import shutil
import database
from tts_utils import parse_text_for_edgetts, get_configured_tts_rate
@@ -24,9 +24,13 @@ def compile_anki_package(records, output_path, deck_name):
Resolves voice models dynamically by gender selection parameters and applies
global speed coefficient rates from the active configurations.
"""
- # Incremented IDs to force a fresh schema mapping without legacy 'Notes' fields
- model_id = 1684329060
- deck_id = 1684329060
+ # 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)
+
+ # Unique timestamp prefix for media files to prevent overwriting prior exports in Anki
+ run_prefix = int(time.time())
# Global Configuration Pace Resolver Mapping
settings = database.load_all_settings() or {}
@@ -95,9 +99,9 @@ def compile_anki_package(records, output_path, deck_name):
# 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 ""
- # Standard Unique Media Filenames
- en_audio_filename = f"edge_en_{idx}_{model_id}.mp3"
- es_audio_filename = f"edge_es_{idx}_{model_id}.mp3"
+ # 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"
en_audio_path = os.path.join(tmpdir, en_audio_filename)
es_audio_path = os.path.join(tmpdir, es_audio_filename)
diff --git a/app.icns b/app.icns
new file mode 100644
index 0000000..2b3410f
Binary files /dev/null and b/app.icns differ
diff --git a/app_icon.iconset/icon_128x128.png b/app_icon.iconset/icon_128x128.png
new file mode 100644
index 0000000..5edaf9a
Binary files /dev/null and b/app_icon.iconset/icon_128x128.png differ
diff --git a/app_icon.iconset/icon_128x128@2x.png b/app_icon.iconset/icon_128x128@2x.png
new file mode 100644
index 0000000..5322e17
Binary files /dev/null and b/app_icon.iconset/icon_128x128@2x.png differ
diff --git a/app_icon.iconset/icon_16x16.png b/app_icon.iconset/icon_16x16.png
new file mode 100644
index 0000000..fa464ff
Binary files /dev/null and b/app_icon.iconset/icon_16x16.png differ
diff --git a/app_icon.iconset/icon_16x16@2x.png b/app_icon.iconset/icon_16x16@2x.png
new file mode 100644
index 0000000..5c75cb3
Binary files /dev/null and b/app_icon.iconset/icon_16x16@2x.png differ
diff --git a/app_icon.iconset/icon_256x256.png b/app_icon.iconset/icon_256x256.png
new file mode 100644
index 0000000..5322e17
Binary files /dev/null and b/app_icon.iconset/icon_256x256.png differ
diff --git a/app_icon.iconset/icon_256x256@2x.png b/app_icon.iconset/icon_256x256@2x.png
new file mode 100644
index 0000000..9fbb2e2
Binary files /dev/null and b/app_icon.iconset/icon_256x256@2x.png differ
diff --git a/app_icon.iconset/icon_32x32.png b/app_icon.iconset/icon_32x32.png
new file mode 100644
index 0000000..5c75cb3
Binary files /dev/null and b/app_icon.iconset/icon_32x32.png differ
diff --git a/app_icon.iconset/icon_32x32@2x.png b/app_icon.iconset/icon_32x32@2x.png
new file mode 100644
index 0000000..c7b3578
Binary files /dev/null and b/app_icon.iconset/icon_32x32@2x.png differ
diff --git a/app_icon.iconset/icon_512x512.png b/app_icon.iconset/icon_512x512.png
new file mode 100644
index 0000000..9fbb2e2
Binary files /dev/null and b/app_icon.iconset/icon_512x512.png differ
diff --git a/app_icon.iconset/icon_512x512@2x.png b/app_icon.iconset/icon_512x512@2x.png
new file mode 100644
index 0000000..32e63ab
Binary files /dev/null and b/app_icon.iconset/icon_512x512@2x.png differ
diff --git a/base_icon.png b/base_icon.png
new file mode 100644
index 0000000..32e63ab
Binary files /dev/null and b/base_icon.png differ
diff --git a/database.py b/database.py
index 01cf016..aabba1a 100644
--- a/database.py
+++ b/database.py
@@ -1,24 +1,79 @@
# database.py
-import sqlite3
import os
+import sys
+import sqlite3
+from PyQt6.QtCore import QSettings
+
+APP_NAME = "SpanishVoiceTrainer"
+DEFAULT_DB_FILENAME = "spanish_trainer.db"
+
+
+def get_default_db_path() -> str:
+ """Returns standard macOS Application Support path:
+ ~/Library/Application Support/SpanishVoiceTrainer/spanish_trainer.db
+ """
+ app_support_dir = os.path.expanduser(
+ f"~/Library/Application Support/{APP_NAME}"
+ )
+ os.makedirs(app_support_dir, exist_ok=True)
+ return os.path.join(app_support_dir, DEFAULT_DB_FILENAME)
+
+
+def get_db_path() -> str:
+ """Retrieves database path cleanly based on execution environment.
+
+ - In packaged app mode (sys.frozen): strictly isolates data inside
+ Application Support unless a valid custom production path is chosen.
+ Heals stale settings pointing to local dev source paths.
+ - In dev mode: allows fallback to local project directory.
+ """
+ qs = QSettings(APP_NAME, "Settings")
+ custom_path = qs.value("database_path", type=str)
+
+ # 1. Check custom path saved in QSettings
+ if custom_path and os.path.exists(custom_path):
+ # Safeguard for packaged production app:
+ # Ignore custom paths that point back into local development source folders
+ if getattr(sys, "frozen", False) and "01_Projects" in custom_path:
+ default_path = get_default_db_path()
+ qs.setValue("database_path", default_path) # Repair stale setting
+ return default_path
+ return custom_path
+
+ # 2. Development mode fallback (uncompiled python runtime)
+ if not getattr(sys, "frozen", False):
+ local_dev_db = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), DEFAULT_DB_FILENAME
+ )
+ if os.path.exists(local_dev_db):
+ return local_dev_db
+
+ # 3. Default production fallback
+ default_path = get_default_db_path()
+ qs.setValue("database_path", default_path)
+ return default_path
+
+
+def set_db_path(new_path: str):
+ """Updates active database path in user preferences."""
+ qs = QSettings(APP_NAME, "Settings")
+ qs.setValue("database_path", new_path)
-DB_NAME = "spanish_trainer.db"
def get_connection():
- """Returns a connection to the SQLite database with row factory enabled."""
- conn = sqlite3.connect(DB_NAME)
+ """Establishes connection to the active SQLite database."""
+ db_path = get_db_path()
+ conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn
+
def ensure_database_populated():
- """
- Creates empty tables using the unified schema if running
- in a fresh environment without a database file.
- """
+ """Initializes tables if running against a new or empty database file."""
conn = get_connection()
cursor = conn.cursor()
try:
- # 1. Unified translations table with dedicated anki_notes and gender tracks
+ # 1. Unified translations table
cursor.execute("""
CREATE TABLE IF NOT EXISTS translations (
translation_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -31,7 +86,7 @@ def ensure_database_populated():
gender TEXT DEFAULT 'Female'
);
""")
-
+
# 2. Key-value configuration table
cursor.execute("""
CREATE TABLE IF NOT EXISTS settings (
@@ -43,10 +98,12 @@ def ensure_database_populated():
finally:
conn.close()
+
# ==========================================
# SETTINGS CRUD FUNCTIONS
# ==========================================
+
def load_all_settings():
"""Fetches all system configuration properties into a flat Python dictionary."""
conn = get_connection()
@@ -60,28 +117,31 @@ def load_all_settings():
conn.close()
return settings_dict
+
def save_setting_to_db(key, value):
"""Inserts or replaces an application configuration entry."""
conn = get_connection()
cursor = conn.cursor()
try:
- cursor.execute("""
+ cursor.execute(
+ """
INSERT OR REPLACE INTO settings (key, value)
VALUES (?, ?);
- """, (key, value))
+ """,
+ (key, value),
+ )
conn.commit()
finally:
conn.close()
+
# ==========================================
# TRANSLATIONS CRUD FUNCTIONS
# ==========================================
+
def get_all_translations_explicit():
- """
- Retrieves all records using completely explicit,
- table-qualified column declarations for the engines.
- """
+ """Retrieves all records using explicit, table-qualified column declarations."""
conn = get_connection()
cursor = conn.cursor()
try:
@@ -102,12 +162,14 @@ def get_all_translations_explicit():
finally:
conn.close()
+
def get_translation_by_id(translation_id):
"""Loads a single unified record row for specific inspection or editing."""
conn = get_connection()
cursor = conn.cursor()
try:
- cursor.execute("""
+ cursor.execute(
+ """
SELECT
translations.translation_id,
translations.es_text,
@@ -119,19 +181,31 @@ def get_translation_by_id(translation_id):
translations.gender
FROM translations
WHERE translations.translation_id = ?;
- """, (translation_id,))
+ """,
+ (translation_id,),
+ )
row = cursor.fetchone()
return dict(row) if row else None
finally:
conn.close()
-def update_translation_record(translation_id, es_text, en_text, source_context, tags, notes, gender, anki_notes):
- """Saves sandbox interface edits directly back down into the table using named arguments."""
+
+def update_translation_record(
+ translation_id,
+ es_text,
+ en_text,
+ source_context,
+ tags,
+ notes,
+ gender,
+ anki_notes,
+):
+ """Saves interface edits directly back into the table using named arguments."""
conn = get_connection()
cursor = conn.cursor()
try:
- # The SQL uses :key syntax instead of ?
- cursor.execute("""
+ cursor.execute(
+ """
UPDATE translations
SET
es_text = :es,
@@ -142,53 +216,62 @@ def update_translation_record(translation_id, es_text, en_text, source_context,
gender = :gender,
anki_notes = :anki
WHERE translation_id = :id;
- """, {
- # The order inside this dictionary does not matter at all!
- "id": translation_id,
- "es": es_text,
- "en": en_text,
- "ctx": source_context,
- "tags": tags,
- "notes": notes,
- "gender": gender,
- "anki": anki_notes
- })
+ """,
+ {
+ "id": translation_id,
+ "es": es_text,
+ "en": en_text,
+ "ctx": source_context,
+ "tags": tags,
+ "notes": notes,
+ "gender": gender,
+ "anki": anki_notes,
+ },
+ )
conn.commit()
finally:
conn.close()
+
def delete_translation_record(translation_id):
"""Permanently drops a phrase card row from the data index."""
conn = get_connection()
cursor = conn.cursor()
try:
- cursor.execute("""
+ cursor.execute(
+ """
DELETE FROM translations
WHERE translations.translation_id = ?;
- """, (translation_id,))
+ """,
+ (translation_id,),
+ )
conn.commit()
finally:
conn.close()
-def insert_translation_record(es_text, en_text, source_context, tags, notes, gender, anki_notes):
- """Inserts a new record using named arguments so positional order doesn't matter."""
- conn = get_connection() # Corrected from get_db_connection
+
+def insert_translation_record(
+ es_text, en_text, source_context, tags, notes, gender, anki_notes
+):
+ """Inserts a new record using named arguments."""
+ conn = get_connection()
cursor = conn.cursor()
try:
- cursor.execute("""
+ cursor.execute(
+ """
INSERT INTO translations (es_text, en_text, source_context, tags, notes, gender, anki_notes)
VALUES (:es, :en, :ctx, :tags, :notes, :gender, :anki);
- """, {
- # SQLite maps these keys directly to the tokens above by name
- "en": en_text,
- "es": es_text,
- "ctx": source_context,
- "tags": tags,
- "notes": notes,
- "anki": anki_notes,
- "gender": gender
- })
+ """,
+ {
+ "en": en_text,
+ "es": es_text,
+ "ctx": source_context,
+ "tags": tags,
+ "notes": notes,
+ "anki": anki_notes,
+ "gender": gender,
+ },
+ )
conn.commit()
finally:
- conn.close()
-
+ conn.close()
\ No newline at end of file
diff --git a/doc/Notes.md b/doc/Notes.md
index 63064ad..579baf5 100644
--- a/doc/Notes.md
+++ b/doc/Notes.md
@@ -19,4 +19,49 @@ git switch -c refactor/simplified-schema-modules
# How to start using uv
-(139_spanish-voice-trainer) stephenlohning@Scotty 139_spanish-voice-trainer % uv run main.py
\ No newline at end of file
+(139_spanish-voice-trainer) stephenlohning@Scotty 139_spanish-voice-trainer % uv run main.py
+
+
+To trim non-relevant files from your directory tree (such as build artifacts, cache files, and non-source assets), you can configure flags for the `tree` command itself or use git-based filtering.
+
+**Tree Command Options**
+
+* **`-I pattern` (Ignore matching patterns):** Exclude build directories, caches, virtual environments, binaries, and temporary files.
+* **`-d` (Directories only):** Show only directory structures if file-level detail is unnecessary.
+* **`-L level` (Limit depth):** Limit how deep `tree` traverses (e.g., `-L 2` or `-L 3`) to prevent showing deep PyInstaller distribution bundles like `dist/` or `build/`.
+* **`--gitignore`:** Automatically respects your `.gitignore` rules (supported in modern `tree` versions).
+
+---
+
+**Recommended Command Patterns**
+
+**1. Ignore standard build & cache artifacts**
+
+```bash
+tree -I "__pycache__|*.pyc|build|dist|*.app|*.iconset|*.png|*.pdf|*.jpg|database_legacy"
+
+```
+
+**2. Combine pattern filtering with limited depth**
+
+```bash
+tree -L 2 -I "__pycache__|build|dist|database_legacy"
+
+```
+
+**3. Use `.gitignore` directly**
+If you already have build outputs, images, and backups ignored in `.gitignore`:
+
+```bash
+tree --gitignore
+
+```
+
+---
+
+**Summary of Files/Folders to Exclude**
+
+* **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
diff --git a/doc/Notes.pdf b/doc/Notes.pdf
index e0bb8c6..4dfe3e0 100644
Binary files a/doc/Notes.pdf and b/doc/Notes.pdf differ
diff --git a/main.spec b/main.spec
new file mode 100644
index 0000000..9f0716a
--- /dev/null
+++ b/main.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='main',
+ 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='main',
+)
+app = BUNDLE(
+ coll,
+ name='main.app',
+ icon='app.icns',
+ bundle_identifier=None,
+)
diff --git a/pyproject.toml b/pyproject.toml
index 087ccc9..21c89e1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,6 +12,7 @@ dependencies = [
"librosa>=0.11.0",
"numpy>=2.4.6",
"pillow>=12.2.0",
+ "pyinstaller>=6.21.0",
"pyqt6>=6.11.0",
"scipy>=1.17.1",
"sounddevice>=0.5.5",
diff --git a/spanish_trainer-26-08-21.db b/spanish_trainer-26-08-21.db
new file mode 100644
index 0000000..1fc4531
Binary files /dev/null and b/spanish_trainer-26-08-21.db differ
diff --git a/spanish_trainer-backup.db b/spanish_trainer-backup.db
new file mode 100644
index 0000000..a61b7e0
Binary files /dev/null and b/spanish_trainer-backup.db differ
diff --git a/spanish_trainer.db b/spanish_trainer.db
index 1fc4531..d7990c4 100644
Binary files a/spanish_trainer.db and b/spanish_trainer.db differ
diff --git a/tabs/review_tab.py b/tabs/review_tab.py
index 75fc449..32e3570 100644
--- a/tabs/review_tab.py
+++ b/tabs/review_tab.py
@@ -1,10 +1,12 @@
# tabs/review_tab.py
import random
import os
+import re
import subprocess
import tempfile
import threading
import asyncio
+from datetime import datetime
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
@@ -338,14 +340,14 @@ class ReviewTab(QWidget):
@pyqtSlot()
def generate_deck_action(self):
- """Generates a specialized lightweight .apkg Anki deck matching active filter parameters,
- respecting exact user database configuration keys for target folders and naming chains."""
+ """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."""
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 your database configurations
+ # Load active settings dictionary directly from database configurations
settings = database.load_all_settings() or {}
# Extract configurations targeting exact database schema names found in settings
@@ -364,17 +366,27 @@ class ReviewTab(QWidget):
if root_deck_name and str(root_deck_name).strip():
deck_tree_parts.append(str(root_deck_name).strip())
- else:
- deck_tree_parts.append("DefaultDeck") # Baseline structural root name fallback
if sub_deck_hierarchy and str(sub_deck_hierarchy).strip():
deck_tree_parts.append(str(sub_deck_hierarchy).strip())
-
- # Join parts using Anki double-colon syntax (::)
+ else:
+ 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)
- # Establish absolute output filename file path anchor
- filename = "Spanish_Filtered_Review.apkg"
+ # --- 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
@@ -383,9 +395,10 @@ class ReviewTab(QWidget):
QMessageBox.information(
self,
"Export Complete",
- f"Successfully exported Anki package to your configured target directory!\n\n"
- f"Full Namespace Tree: {full_deck_namespace}\n"
- f"Destination Path: {file_path}"
+ f"Successfully exported Anki package!\n\n"
+ f"Deck Hierarchy: {full_deck_namespace}\n"
+ f"File Name: {filename}\n"
+ f"Path: {file_path}"
)
except Exception as e:
QMessageBox.critical(self, "Compiler Fault Safeguard", f"An exception occurred building your deck container package:\n{str(e)}")
diff --git a/tabs/settings_tab.py b/tabs/settings_tab.py
index 40eec84..fe4f5f5 100644
--- a/tabs/settings_tab.py
+++ b/tabs/settings_tab.py
@@ -19,6 +19,29 @@ class SettingsTab(QWidget):
main_layout = QVBoxLayout(self)
main_layout.setSpacing(15)
+
+ # --- SECTION 0: DATABASE FILE CONFIGURATION ---
+ db_group = QGroupBox("Database Storage Configuration")
+ db_form = QFormLayout(db_group)
+ db_form.setSpacing(10)
+
+ db_picker_layout = QHBoxLayout()
+ self.txt_db_path = QLineEdit()
+ self.txt_db_path.setReadOnly(True)
+ self.txt_db_path.setStyleSheet("background-color: #F8F9F9; color: #34495E;")
+
+ btn_browse_db = QPushButton("Browse...")
+ btn_browse_db.clicked.connect(self.browse_database_file)
+
+ btn_reset_db = QPushButton("Reset Default")
+ btn_reset_db.clicked.connect(self.reset_default_database)
+
+ db_picker_layout.addWidget(self.txt_db_path)
+ db_picker_layout.addWidget(btn_browse_db)
+ db_picker_layout.addWidget(btn_reset_db)
+
+ db_form.addRow("Active Database File:", db_picker_layout)
+ main_layout.addWidget(db_group)
# --- SECTION 1: GLOBAL ANKI PACKAGING CONFIGURATIONS ---
anki_group = QGroupBox("Anki Compilation Settings")
@@ -59,7 +82,7 @@ class SettingsTab(QWidget):
self.txt_tts_voice.textChanged.connect(lambda text: self.update_setting("tts_preferred_voice", text.strip()))
self.txt_tts_speed = QLineEdit()
- self.txt_tts_speed.setPlaceholderText("1.15")
+ self.txt_tts_speed.setPlaceholderText("0.75")
self.txt_tts_speed.textChanged.connect(lambda text: self.update_setting("tts_playback_speed", text.strip()))
engine_form.addRow("Fallback System Voice Name:", self.txt_tts_voice)
@@ -106,13 +129,16 @@ class SettingsTab(QWidget):
# Block signals briefly so loading state doesn't trigger write-back loops
self.blockSignals(True)
+ # Populate active database path
+ self.txt_db_path.setText(database.get_db_path())
+
stored_settings = database.load_all_settings()
self.txt_root_deck.setText(stored_settings.get("anki_root_deck_name", "Spanish"))
self.txt_sub_deck.setText(stored_settings.get("anki_sub_deck_name", ""))
self.txt_export_dir.setText(stored_settings.get("anki_export_directory", os.path.expanduser("~")))
self.txt_tts_voice.setText(stored_settings.get("tts_preferred_voice", "Apple_Monica"))
- self.txt_tts_speed.setText(stored_settings.get("tts_playback_speed", "1.15"))
+ self.txt_tts_speed.setText(stored_settings.get("tts_playback_speed", "0.75"))
self.blockSignals(False)
@@ -120,6 +146,37 @@ class SettingsTab(QWidget):
"""Internal helper to communicate state mutations instantly upward."""
self.settings_changed.emit(key, value)
+ def browse_database_file(self):
+ """Allows user to choose an existing SQLite database file or create a new one."""
+ current_db = self.txt_db_path.text()
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ "Select SQLite Database File",
+ current_db,
+ "SQLite Database (*.db *.sqlite *.sqlite3);;All Files (*)",
+ )
+ if file_path:
+ database.set_db_path(file_path)
+ database.ensure_database_populated()
+ self.populate_fields_from_db_state()
+ QMessageBox.information(
+ self,
+ "Database Switched",
+ f"Active database switched to:\n{file_path}",
+ )
+
+ def reset_default_database(self):
+ """Resets the database path back to macOS Application Support default directory."""
+ default_path = database.get_default_db_path()
+ database.set_db_path(default_path)
+ database.ensure_database_populated()
+ self.populate_fields_from_db_state()
+ QMessageBox.information(
+ self,
+ "Database Reset",
+ f"Reset database path to default location:\n{default_path}",
+ )
+
def browse_export_directory(self):
"""Invokes a native macOS directory finder path browser window."""
current_dir = self.txt_export_dir.text() or os.path.expanduser("~")
diff --git a/tree.txt b/tree.txt
new file mode 100644
index 0000000..62b2776
--- /dev/null
+++ b/tree.txt
@@ -0,0 +1,981 @@
+.
+├── 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/tree2.txt b/tree2.txt
new file mode 100644
index 0000000..14de4dc
--- /dev/null
+++ b/tree2.txt
@@ -0,0 +1,69 @@
+.
+├── LICENSE
+├── README.md
+├── README.pdf
+├── Santiago_cathedral_2021_Sunset.jpg
+├── 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
+├── core
+│ ├── __init__.py
+│ ├── asset_generator.py
+│ ├── audio_engine.py
+│ ├── bulk_importer.py
+│ ├── clean_glossary.py
+│ └── phrase_manager.py
+├── database.py
+├── doc
+│ ├── Notes.md
+│ ├── Notes.pdf
+│ └── images
+├── 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
+│ ├── review_tab.py
+│ ├── sandbox_tab.py
+│ └── settings_tab.py
+├── test_dual.py
+├── test_tts.py
+├── tree.txt
+├── tree2.txt
+├── tts_utils.py
+├── ui
+│ ├── __init__.py
+│ ├── cli
+│ └── gui
+├── uv.lock
+└── video_generator.py
+
+10 directories, 57 files
diff --git a/uv.lock b/uv.lock
index 04d72ca..244f1be 100644
--- a/uv.lock
+++ b/uv.lock
@@ -24,6 +24,7 @@ dependencies = [
{ name = "librosa" },
{ name = "numpy" },
{ name = "pillow" },
+ { name = "pyinstaller" },
{ name = "pyqt6" },
{ name = "scipy" },
{ name = "sounddevice" },
@@ -38,6 +39,7 @@ requires-dist = [
{ name = "librosa", specifier = ">=0.11.0" },
{ name = "numpy", specifier = ">=2.4.6" },
{ name = "pillow", specifier = ">=12.2.0" },
+ { name = "pyinstaller", specifier = ">=6.21.0" },
{ name = "pyqt6", specifier = ">=6.11.0" },
{ name = "scipy", specifier = ">=1.17.1" },
{ name = "sounddevice", specifier = ">=0.5.5" },
@@ -163,6 +165,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
]
+[[package]]
+name = "altgraph"
+version = "0.17.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" },
+]
+
[[package]]
name = "annotated-doc"
version = "0.0.4"
@@ -1152,6 +1163,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" },
]
+[[package]]
+name = "macholib"
+version = "1.16.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "altgraph", marker = "sys_platform == 'darwin'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" },
+]
+
[[package]]
name = "mail-parser"
version = "4.4.0"
@@ -1744,6 +1767,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" },
]
+[[package]]
+name = "pefile"
+version = "2024.8.26"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
+]
+
[[package]]
name = "pillow"
version = "12.2.0"
@@ -2079,6 +2111,46 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
+[[package]]
+name = "pyinstaller"
+version = "6.21.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "altgraph" },
+ { name = "macholib", marker = "sys_platform == 'darwin'" },
+ { name = "packaging" },
+ { name = "pefile", marker = "sys_platform == 'win32'" },
+ { name = "pyinstaller-hooks-contrib" },
+ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
+ { name = "setuptools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d5/4d/ec706c3fcf39e26888c35b39615ff4d5865d184069666c47492cff1fbe50/pyinstaller-6.21.0.tar.gz", hash = "sha256:bb9fab705983e393a2d1cac77d6972513057ad800215fd861dc15ff5272e98fd", size = 4061519, upload-time = "2026-06-13T14:15:06.25Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/4a/53cf98bf66daed012dc9cd78c8203f19a675d696f2fc12afcf8c5049a0e0/pyinstaller-6.21.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:327d132389f37912609e01be62810cf96b5aa95b613903e4b8692e0d12fb0eda", size = 1052350, upload-time = "2026-06-13T14:13:55.88Z" },
+ { url = "https://files.pythonhosted.org/packages/30/83/b591295c352ef464c50b4c6ffff1c4f771d875c9e833f578d1b9f564f6b3/pyinstaller-6.21.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7071d4b094d5b40deeef5fa3d3b98a1b846087f7562b49209663d5f9281fe251", size = 748477, upload-time = "2026-06-13T14:14:00.327Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/8f/88fff4e403873b1e22286911350e75ff00db014aa08e57045da9d4328993/pyinstaller-6.21.0-py3-none-manylinux2014_i686.whl", hash = "sha256:6b6374d652107dd4a2eeece903ff82bb4045bb5e1006c5a158a6dcdbefe84bf2", size = 760877, upload-time = "2026-06-13T14:14:04.836Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/13/f0e48fbdfd1d05d948157121cea8b1b823dcb89efe6934b71fdd8bdb3f0f/pyinstaller-6.21.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4e3108b3f02384560da70e39b8bf22b0ad597d02bd68a40d76ea91c1cfa00cad", size = 759194, upload-time = "2026-06-13T14:14:10.61Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d5/ea7878cf9924ed30d946d8288777424e6d069d94f5bde56b4d0890069664/pyinstaller-6.21.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:697532279f535ad572bda613db4f821540e235c7854ca6da4d3bf0373f4415ee", size = 754979, upload-time = "2026-06-13T14:14:15.226Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/09/51b8905714b733bac66dbc041a7821372d70d888d273ae474c4037d4202d/pyinstaller-6.21.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:605169523a6b5ace39f13dfbff21add9f2bc43df99c7daf9394fefb2c45e8b6f", size = 754812, upload-time = "2026-06-13T14:14:20.264Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/43/d77779439d8c6c2e27a77bcfbd1d5cc0f568ebb611bb472b11af81b5f177/pyinstaller-6.21.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5fa56746c1e76f93634d018502301378a2d0c382553d37d8c3c34ff436c12dd1", size = 753887, upload-time = "2026-06-13T14:14:25.268Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8f/c22df1f6837784ac349057ba693f08e7b1ca7a0e06f9c33c63bc6280007b/pyinstaller-6.21.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:42395ec76df8e8120c36b13339d9db8cab83e316a12839ee303cc00fc941bb74", size = 753779, upload-time = "2026-06-13T14:14:29.445Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/76/1ce8a27ce62ba8cf3a87c9ce6d575610f4e55d7cb0123e7512fc3f4b921a/pyinstaller-6.21.0-py3-none-win32.whl", hash = "sha256:c6b28d30d8fd99ce162ff3aab5013ed44dbfb747566b1f01b9bed7964d7c14e9", size = 1336462, upload-time = "2026-06-13T14:14:35.785Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/fa/ca1d7e5257dd8566a9dfc0dfb02f8a8075eeb53d4b2d3c579f1276759042/pyinstaller-6.21.0-py3-none-win_amd64.whl", hash = "sha256:7fae06c494ce0ebfe6bd3055c0e409def884f63af2e3705d06bd431ad9237fc7", size = 1397487, upload-time = "2026-06-13T14:14:42.328Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/75/21b51523ce8d96629b71311775a0a65f5f5a872124ab0de33e5c848f8bff/pyinstaller-6.21.0-py3-none-win_arm64.whl", hash = "sha256:f13c95c9c03fb567217135919f93815c305813126780b0ed6e0123cb8acaf025", size = 1346094, upload-time = "2026-06-13T14:14:48.914Z" },
+]
+
+[[package]]
+name = "pyinstaller-hooks-contrib"
+version = "2026.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/94/5b/c9fe0db5e83ee1c39b2258fa21d23b15e1a60786b6c5990ee5074ead8bb6/pyinstaller_hooks_contrib-2026.6.tar.gz", hash = "sha256:bef5002c32f4f50bd55b005da12cff64eca8783e7eaf86a06a62410164bab725", size = 173354, upload-time = "2026-06-08T22:37:16.152Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/31/f2d7343d8ed5f7c4678377886f6ce533e6eaaa131b252ce950114c2a7efa/pyinstaller_hooks_contrib-2026.6-py3-none-any.whl", hash = "sha256:fd13b8ac126b35361175edacd41a0d97080b75dd5f4b594ecefefff969509dd3", size = 457159, upload-time = "2026-06-08T22:37:14.722Z" },
+]
+
[[package]]
name = "pylatexenc"
version = "2.10"
@@ -2227,6 +2299,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" },
]
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
+]
+
[[package]]
name = "pyyaml"
version = "6.0.3"