Compare commits
No commits in common. "refactor/simplified-schema-modules" and "main" have entirely different histories.
refactor/s
...
main
4
.gitignore
vendored
|
|
@ -5,10 +5,6 @@ __pycache__/
|
||||||
.venv/
|
.venv/
|
||||||
.uv/
|
.uv/
|
||||||
|
|
||||||
dist/*
|
|
||||||
build/*
|
|
||||||
|
|
||||||
|
|
||||||
# Local SQLite Databases
|
# Local SQLite Databases
|
||||||
# *.db
|
# *.db
|
||||||
# *.db-journal
|
# *.db-journal
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 13 MiB |
141
anki_exporter.py
|
|
@ -1,141 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
||||||
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.
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 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 {}
|
|
||||||
rate_string = get_configured_tts_rate(settings)
|
|
||||||
|
|
||||||
anki_model = genanki.Model(
|
|
||||||
model_id,
|
|
||||||
'Spanish Bidirectional Multi-Note HTML Model',
|
|
||||||
fields=[
|
|
||||||
{'name': 'SortOrder'}, # Field 0: Anki primary sort field
|
|
||||||
{'name': 'EnglishText'},
|
|
||||||
{'name': 'SpanishText'},
|
|
||||||
{'name': 'AnkiNotes'},
|
|
||||||
{'name': 'EnglishAudio'},
|
|
||||||
{'name': 'SpanishAudio'}
|
|
||||||
],
|
|
||||||
templates=[
|
|
||||||
{
|
|
||||||
'name': 'Card 1: English ➔ Spanish',
|
|
||||||
'qfmt': '<div style="font-family: Arial; font-size: 13px; font-weight: bold; color: #BDC3C7; text-align: center; letter-spacing: 1px;">TRANSLATE TO SPANISH:</div><br>'
|
|
||||||
'<div style="font-family: Arial; font-size: 20px; text-align: center; color: #34495E;"><b>{{EnglishText}}</b></div>'
|
|
||||||
'<div style="display:none;">{{EnglishAudio}}</div>',
|
|
||||||
'afmt': '{{FrontSide}}<hr id="answer">'
|
|
||||||
'<div style="font-family: Arial; font-size: 28px; text-align: center; color: #2980B9; font-weight: bold;">{{SpanishText}}</div><br>'
|
|
||||||
'{{#AnkiNotes}}<div style="font-family: Arial; font-size: 13px; text-align: center; color: #8E44AD; border-top: 1px dashed #E5E7E9; padding-top: 6px; margin-top: 6px;"><b>Anki Meta:</b> {{{AnkiNotes}}}</div>{{/AnkiNotes}}<br>'
|
|
||||||
'<div style="text-align: center;">{{SpanishAudio}}</div>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Card 2: Spanish ➔ English',
|
|
||||||
'qfmt': '<div style="font-family: Arial; font-size: 13px; font-weight: bold; color: #E67E22; text-align: center; letter-spacing: 1px;">TRANSLATE TO ENGLISH:</div><br>'
|
|
||||||
'<div style="font-family: Arial; font-size: 26px; text-align: center; color: #2980B9; font-weight: bold;"><b>{{SpanishText}}</b></div>'
|
|
||||||
'<div style="display:none;">{{SpanishAudio}}</div>',
|
|
||||||
'afmt': '{{FrontSide}}<hr id="answer">'
|
|
||||||
'<div style="font-family: Arial; font-size: 22px; text-align: center; color: #2C3E50; font-weight: 500;">{{EnglishText}}</div><br>'
|
|
||||||
'{{#AnkiNotes}}<div style="font-family: Arial; font-size: 13px; text-align: center; color: #8E44AD; border-top: 1px dashed #E5E7E9; padding-top: 6px; margin-top: 6px;"><b>Anki Meta:</b> {{{AnkiNotes}}}</div>{{/AnkiNotes}}<br>'
|
|
||||||
'<div style="text-align: center;">{{EnglishAudio}}</div>',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
deck = genanki.Deck(deck_id, deck_name)
|
|
||||||
media_files_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"]
|
|
||||||
es_raw = record["es_text"]
|
|
||||||
gender_flag = record.get("gender", "Female")
|
|
||||||
|
|
||||||
# Read sort_order from database record (fallback to loop index if unset)
|
|
||||||
raw_order = record.get("sort_order")
|
|
||||||
order_val = int(raw_order) if raw_order is not None else (idx + 1)
|
|
||||||
sort_order_str = f"{order_val:04d}" # Zero-padded integer string for exact sorting
|
|
||||||
|
|
||||||
# Sanitize text payloads for TTS engine
|
|
||||||
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"
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
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:
|
|
||||||
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:
|
|
||||||
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],
|
|
||||||
tags=note_tags
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
BIN
app.icns
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 524 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 524 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
BIN
base_icon.png
|
Before Width: | Height: | Size: 1.8 MiB |
290
database.py
|
|
@ -1,290 +0,0 @@
|
||||||
# database.py
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def get_connection():
|
|
||||||
"""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():
|
|
||||||
"""Initializes tables and migrates schemas if running against a new or existing database file."""
|
|
||||||
conn = get_connection()
|
|
||||||
cursor = conn.cursor()
|
|
||||||
try:
|
|
||||||
# 1. Unified translations table
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS translations (
|
|
||||||
translation_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
es_text TEXT NOT NULL,
|
|
||||||
en_text TEXT NOT NULL,
|
|
||||||
source_context TEXT,
|
|
||||||
tags TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
anki_notes TEXT DEFAULT '',
|
|
||||||
gender TEXT DEFAULT 'Female',
|
|
||||||
sort_order INTEGER DEFAULT 0
|
|
||||||
);
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Safely migrate existing databases that do not yet have the sort_order column
|
|
||||||
cursor.execute("PRAGMA table_info(translations);")
|
|
||||||
columns = [row["name"] for row in cursor.fetchall()]
|
|
||||||
if "sort_order" not in columns:
|
|
||||||
cursor.execute("ALTER TABLE translations ADD COLUMN sort_order INTEGER DEFAULT 0;")
|
|
||||||
|
|
||||||
# 2. Key-value configuration table
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT
|
|
||||||
);
|
|
||||||
""")
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# SETTINGS CRUD FUNCTIONS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
|
|
||||||
def load_all_settings():
|
|
||||||
"""Fetches all system configuration properties into a flat Python dictionary."""
|
|
||||||
conn = get_connection()
|
|
||||||
cursor = conn.cursor()
|
|
||||||
settings_dict = {}
|
|
||||||
try:
|
|
||||||
cursor.execute("SELECT settings.key, settings.value FROM settings;")
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
settings_dict[row["key"]] = row["value"]
|
|
||||||
finally:
|
|
||||||
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(
|
|
||||||
"""
|
|
||||||
INSERT OR REPLACE INTO settings (key, value)
|
|
||||||
VALUES (?, ?);
|
|
||||||
""",
|
|
||||||
(key, value),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# TRANSLATIONS CRUD FUNCTIONS
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_translations_explicit():
|
|
||||||
"""Retrieves all records using explicit, table-qualified column declarations sorted by sort_order."""
|
|
||||||
conn = get_connection()
|
|
||||||
cursor = conn.cursor()
|
|
||||||
try:
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT
|
|
||||||
translations.translation_id,
|
|
||||||
translations.es_text,
|
|
||||||
translations.en_text,
|
|
||||||
translations.source_context,
|
|
||||||
translations.tags,
|
|
||||||
translations.notes,
|
|
||||||
translations.anki_notes,
|
|
||||||
translations.gender,
|
|
||||||
translations.sort_order
|
|
||||||
FROM translations
|
|
||||||
ORDER BY translations.sort_order ASC, translations.translation_id ASC;
|
|
||||||
""")
|
|
||||||
return [dict(row) for row in cursor.fetchall()]
|
|
||||||
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(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
translations.translation_id,
|
|
||||||
translations.es_text,
|
|
||||||
translations.en_text,
|
|
||||||
translations.source_context,
|
|
||||||
translations.tags,
|
|
||||||
translations.notes,
|
|
||||||
translations.anki_notes,
|
|
||||||
translations.gender,
|
|
||||||
translations.sort_order
|
|
||||||
FROM translations
|
|
||||||
WHERE translations.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,
|
|
||||||
sort_order=0,
|
|
||||||
):
|
|
||||||
"""Saves interface edits directly back into the table using named arguments."""
|
|
||||||
conn = get_connection()
|
|
||||||
cursor = conn.cursor()
|
|
||||||
try:
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
UPDATE translations
|
|
||||||
SET
|
|
||||||
es_text = :es,
|
|
||||||
en_text = :en,
|
|
||||||
source_context = :ctx,
|
|
||||||
tags = :tags,
|
|
||||||
notes = :notes,
|
|
||||||
gender = :gender,
|
|
||||||
anki_notes = :anki,
|
|
||||||
sort_order = :sort_order
|
|
||||||
WHERE translation_id = :id;
|
|
||||||
""",
|
|
||||||
{
|
|
||||||
"id": translation_id,
|
|
||||||
"es": es_text,
|
|
||||||
"en": en_text,
|
|
||||||
"ctx": source_context,
|
|
||||||
"tags": tags,
|
|
||||||
"notes": notes,
|
|
||||||
"gender": gender,
|
|
||||||
"anki": anki_notes,
|
|
||||||
"sort_order": sort_order,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
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(
|
|
||||||
"""
|
|
||||||
DELETE FROM translations
|
|
||||||
WHERE translations.translation_id = ?;
|
|
||||||
""",
|
|
||||||
(translation_id,),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
def insert_translation_record(
|
|
||||||
es_text, en_text, source_context, tags, notes, gender, anki_notes, sort_order=0
|
|
||||||
):
|
|
||||||
"""Inserts a new record using named arguments."""
|
|
||||||
conn = get_connection()
|
|
||||||
cursor = conn.cursor()
|
|
||||||
try:
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO translations (es_text, en_text, source_context, tags, notes, gender, anki_notes, sort_order)
|
|
||||||
VALUES (:es, :en, :ctx, :tags, :notes, :gender, :anki, :sort_order);
|
|
||||||
""",
|
|
||||||
{
|
|
||||||
"en": en_text,
|
|
||||||
"es": es_text,
|
|
||||||
"ctx": source_context,
|
|
||||||
"tags": tags,
|
|
||||||
"notes": notes,
|
|
||||||
"anki": anki_notes,
|
|
||||||
"gender": gender,
|
|
||||||
"sort_order": sort_order,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
1142
doc/Notes.md
BIN
doc/Notes.pdf
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 59 KiB |
51
main.spec
|
|
@ -1,51 +0,0 @@
|
||||||
# -*- 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,
|
|
||||||
)
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
# migrate_database.py
|
|
||||||
import sqlite3
|
|
||||||
import os
|
|
||||||
|
|
||||||
OLD_DB = "spanish_trainer_legacy.db" # Your existing database renamed
|
|
||||||
NEW_DB = "spanish_trainer.db" # The fresh, simplified target database
|
|
||||||
|
|
||||||
def migrate():
|
|
||||||
if not os.path.exists(OLD_DB):
|
|
||||||
print(f"❌ Error: Could not find legacy database file named '{OLD_DB}'")
|
|
||||||
print("Please rename your active database file to match before running this script.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("🚀 Initializing schema transformation...")
|
|
||||||
|
|
||||||
# Connect to both databases
|
|
||||||
conn_old = sqlite3.connect(OLD_DB)
|
|
||||||
conn_old.row_factory = sqlite3.Row
|
|
||||||
cursor_old = conn_old.cursor()
|
|
||||||
|
|
||||||
conn_new = sqlite3.connect(NEW_DB)
|
|
||||||
cursor_new = conn_new.cursor()
|
|
||||||
|
|
||||||
# 1. Provision the clean, simplified new tables
|
|
||||||
cursor_new.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS translations (
|
|
||||||
translation_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
es_text TEXT NOT NULL,
|
|
||||||
en_text TEXT NOT NULL,
|
|
||||||
source_context TEXT,
|
|
||||||
tags TEXT,
|
|
||||||
notes TEXT
|
|
||||||
);
|
|
||||||
""")
|
|
||||||
|
|
||||||
cursor_new.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT
|
|
||||||
);
|
|
||||||
""")
|
|
||||||
|
|
||||||
# 2. Extract and pair data using explicit, table-qualified SQL queries
|
|
||||||
print("📦 Extracting and consolidating relational text rows...")
|
|
||||||
migration_query = """
|
|
||||||
SELECT
|
|
||||||
t.translation_id,
|
|
||||||
p1.text AS spanish_phrase,
|
|
||||||
p2.text AS english_translation,
|
|
||||||
p1.source_context AS textbook_unit,
|
|
||||||
t.tags AS metadata_tags,
|
|
||||||
t.notes AS historical_notes
|
|
||||||
FROM translations t
|
|
||||||
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
|
||||||
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
|
||||||
WHERE p1.language = 'es'
|
|
||||||
AND p2.language = 'en'
|
|
||||||
ORDER BY t.translation_id ASC;
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
cursor_old.execute(migration_query)
|
|
||||||
legacy_records = cursor_old.fetchall()
|
|
||||||
except sqlite3.OperationalError as e:
|
|
||||||
print(f"❌ Legacy structure lookup failed: {e}")
|
|
||||||
print("Verify your old table structures match the schema before running.")
|
|
||||||
conn_old.close()
|
|
||||||
conn_new.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# 3. Insert records into the new simplified table structure
|
|
||||||
inserted_count = 0
|
|
||||||
for row in legacy_records:
|
|
||||||
cursor_new.execute("""
|
|
||||||
INSERT INTO translations (
|
|
||||||
translation_id,
|
|
||||||
es_text,
|
|
||||||
en_text,
|
|
||||||
source_context,
|
|
||||||
tags,
|
|
||||||
notes
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?);
|
|
||||||
""", (
|
|
||||||
row["translation_id"],
|
|
||||||
row["spanish_phrase"],
|
|
||||||
row["english_translation"],
|
|
||||||
row["textbook_unit"],
|
|
||||||
row["metadata_tags"],
|
|
||||||
row["historical_notes"]
|
|
||||||
))
|
|
||||||
inserted_count += 1
|
|
||||||
|
|
||||||
# 4. Copy existing system configuration keys over safely
|
|
||||||
try:
|
|
||||||
cursor_old.execute("SELECT key, value FROM settings;")
|
|
||||||
settings_records = cursor_old.fetchall()
|
|
||||||
for setting in settings_records:
|
|
||||||
cursor_new.execute("""
|
|
||||||
INSERT OR REPLACE INTO settings (key, value)
|
|
||||||
VALUES (?, ?);
|
|
||||||
""", (setting["key"], setting["value"]))
|
|
||||||
except sqlite3.OperationalError:
|
|
||||||
print("⚠️ Warning: No legacy settings table found or could not read it. Skipping settings copy.")
|
|
||||||
|
|
||||||
# Commit changes and clean up connections
|
|
||||||
conn_new.commit()
|
|
||||||
conn_old.close()
|
|
||||||
conn_new.close()
|
|
||||||
|
|
||||||
print(f"✨ Migration complete! Successfully converted {inserted_count} text rows.")
|
|
||||||
print(f"💾 Fresh database engine ready at: {NEW_DB}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
migrate()
|
|
||||||
|
|
@ -12,7 +12,6 @@ dependencies = [
|
||||||
"librosa>=0.11.0",
|
"librosa>=0.11.0",
|
||||||
"numpy>=2.4.6",
|
"numpy>=2.4.6",
|
||||||
"pillow>=12.2.0",
|
"pillow>=12.2.0",
|
||||||
"pyinstaller>=6.21.0",
|
|
||||||
"pyqt6>=6.11.0",
|
"pyqt6>=6.11.0",
|
||||||
"scipy>=1.17.1",
|
"scipy>=1.17.1",
|
||||||
"sounddevice>=0.5.5",
|
"sounddevice>=0.5.5",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# tabs/__init__.py
|
|
||||||
# Leave this file empty, or just expose the tabs like this:
|
|
||||||
from .sandbox_tab import SandboxTab
|
|
||||||
from .review_tab import ReviewTab
|
|
||||||
from .settings_tab import SettingsTab
|
|
||||||
|
|
@ -1,413 +0,0 @@
|
||||||
# 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,
|
|
||||||
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
|
|
||||||
|
|
||||||
class ReviewTab(QWidget):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
# Core Review State Tracking
|
|
||||||
self.all_cached_records = []
|
|
||||||
self.filtered_review_pool = []
|
|
||||||
self.current_index = -1
|
|
||||||
self.is_flipped = False # Track front vs back state of the active flashcard
|
|
||||||
|
|
||||||
# Primary Main Layout
|
|
||||||
main_layout = QVBoxLayout(self)
|
|
||||||
main_layout.setContentsMargins(30, 20, 30, 20)
|
|
||||||
main_layout.setSpacing(15)
|
|
||||||
|
|
||||||
# --- SECTION 1: TOP REGION (Source Context & Tags Filters) ---
|
|
||||||
top_container = QWidget()
|
|
||||||
top_layout = QFormLayout(top_container)
|
|
||||||
top_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
|
||||||
top_layout.setSpacing(10)
|
|
||||||
|
|
||||||
self.txt_review_context = QLineEdit()
|
|
||||||
self.txt_review_context.setPlaceholderText("Filter deck by context (e.g., Camino 2027)...")
|
|
||||||
self.txt_review_context.setStyleSheet("padding: 6px; font-size: 14px;")
|
|
||||||
self.txt_review_context.textChanged.connect(self.handle_live_filter)
|
|
||||||
|
|
||||||
self.txt_review_tags = QLineEdit()
|
|
||||||
self.txt_review_tags.setPlaceholderText("Filter deck by tags (e.g., verb, greeting)...")
|
|
||||||
self.txt_review_tags.setStyleSheet("padding: 6px; font-size: 14px;")
|
|
||||||
self.txt_review_tags.textChanged.connect(self.handle_live_filter)
|
|
||||||
|
|
||||||
top_layout.addRow(QLabel("<b>Source Context:</b>"), self.txt_review_context)
|
|
||||||
top_layout.addRow(QLabel("<b>Tags:</b>"), self.txt_review_tags)
|
|
||||||
|
|
||||||
main_layout.addWidget(top_container)
|
|
||||||
|
|
||||||
# --- SECTION 2: MIDDLE REGION (Split Screen Workspace) ---
|
|
||||||
split_layout = QHBoxLayout()
|
|
||||||
split_layout.setSpacing(20)
|
|
||||||
|
|
||||||
# Left Half: Live Translation Grid View Table
|
|
||||||
self.table = QTableWidget()
|
|
||||||
self.table.setColumnCount(2)
|
|
||||||
self.table.setHorizontalHeaderLabels(["English Phrase", "Spanish Translation"])
|
|
||||||
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
||||||
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
|
||||||
|
|
||||||
header = self.table.horizontalHeader()
|
|
||||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
||||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
||||||
|
|
||||||
split_layout.addWidget(self.table, stretch=1)
|
|
||||||
|
|
||||||
# Right Half: Live Interactive Flashcard Review Panel container
|
|
||||||
card_container = QWidget()
|
|
||||||
card_vbox = QVBoxLayout(card_container)
|
|
||||||
card_vbox.setContentsMargins(0, 0, 0, 0)
|
|
||||||
card_vbox.setSpacing(12)
|
|
||||||
|
|
||||||
# The Card Visual Canvas Frame
|
|
||||||
self.card_frame = QFrame()
|
|
||||||
self.card_frame.setStyleSheet("""
|
|
||||||
QFrame {
|
|
||||||
background-color: #FAFAFA;
|
|
||||||
border: 2px solid #E5E7E9;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
card_frame_layout = QVBoxLayout(self.card_frame)
|
|
||||||
card_frame_layout.setContentsMargins(25, 25, 25, 25)
|
|
||||||
|
|
||||||
self.card_stack = QStackedWidget()
|
|
||||||
|
|
||||||
# Card Front View (English Prompt)
|
|
||||||
self.view_front = QWidget()
|
|
||||||
front_layout = QVBoxLayout(self.view_front)
|
|
||||||
front_prompt = QLabel("TRANSLATE TO SPANISH:")
|
|
||||||
front_prompt.setStyleSheet("font-size: 11px; font-weight: bold; color: #BDC3C7; letter-spacing: 1px;")
|
|
||||||
front_prompt.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
|
|
||||||
self.lbl_english = QLabel("No cards matching active filters.")
|
|
||||||
self.lbl_english.setStyleSheet("font-size: 20px; color: #34495E; font-weight: 500; margin-top: 15px;")
|
|
||||||
self.lbl_english.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
self.lbl_english.setWordWrap(True)
|
|
||||||
|
|
||||||
front_layout.addWidget(front_prompt)
|
|
||||||
front_layout.addWidget(self.lbl_english)
|
|
||||||
front_layout.addStretch()
|
|
||||||
|
|
||||||
# Card Back View (Spanish Answer Only)
|
|
||||||
self.view_back = QWidget()
|
|
||||||
back_layout = QVBoxLayout(self.view_back)
|
|
||||||
|
|
||||||
self.lbl_spanish = QLabel("Spanish Answer Text")
|
|
||||||
self.lbl_spanish.setStyleSheet("font-size: 24px; font-weight: bold; color: #2980B9; margin-top: 20px;")
|
|
||||||
self.lbl_spanish.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
self.lbl_spanish.setWordWrap(True)
|
|
||||||
|
|
||||||
back_layout.addWidget(self.lbl_spanish)
|
|
||||||
back_layout.addStretch()
|
|
||||||
|
|
||||||
self.card_stack.addWidget(self.view_front)
|
|
||||||
self.card_stack.addWidget(self.view_back)
|
|
||||||
card_frame_layout.addWidget(self.card_stack)
|
|
||||||
|
|
||||||
card_vbox.addWidget(self.card_frame, stretch=1)
|
|
||||||
|
|
||||||
# Buttons Row beneath the Flashcard
|
|
||||||
card_buttons_layout = QHBoxLayout()
|
|
||||||
card_buttons_layout.setSpacing(10)
|
|
||||||
|
|
||||||
self.btn_play_audio = QPushButton("🔊 Play Voice")
|
|
||||||
self.btn_play_audio.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
self.btn_play_audio.setStyleSheet("""
|
|
||||||
QPushButton { background-color: #E67E22; color: white; font-weight: bold; font-size: 13px; padding: 10px; border-radius: 4px; }
|
|
||||||
QPushButton:hover { background-color: #D35400; }
|
|
||||||
""")
|
|
||||||
self.btn_play_audio.clicked.connect(self.play_card_audio)
|
|
||||||
|
|
||||||
self.btn_flip_next = QPushButton("Flip Card")
|
|
||||||
self.btn_flip_next.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
self.btn_flip_next.setStyleSheet("""
|
|
||||||
QPushButton { background-color: #34495E; color: white; font-weight: bold; font-size: 13px; padding: 10px; border-radius: 4px; }
|
|
||||||
QPushButton:hover { background-color: #2C3E50; }
|
|
||||||
""")
|
|
||||||
self.btn_flip_next.clicked.connect(self.handle_card_interaction)
|
|
||||||
|
|
||||||
card_buttons_layout.addWidget(self.btn_play_audio, stretch=1)
|
|
||||||
card_buttons_layout.addWidget(self.btn_flip_next, stretch=2)
|
|
||||||
card_vbox.addLayout(card_buttons_layout)
|
|
||||||
|
|
||||||
split_layout.addWidget(card_container, stretch=1)
|
|
||||||
main_layout.addLayout(split_layout)
|
|
||||||
|
|
||||||
# --- SECTION 3: BOTTOM REGION (Action Control Panel) ---
|
|
||||||
bottom_layout = QHBoxLayout()
|
|
||||||
bottom_layout.setSpacing(15)
|
|
||||||
|
|
||||||
self.btn_generate_deck = QPushButton("🗂️ Generate Deck")
|
|
||||||
self.btn_generate_deck.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
self.btn_generate_deck.setStyleSheet("""
|
|
||||||
QPushButton { background-color: #27AE60; color: white; font-weight: bold; font-size: 14px; padding: 10px 22px; border-radius: 5px; }
|
|
||||||
QPushButton:hover { background-color: #219653; }
|
|
||||||
""")
|
|
||||||
self.btn_generate_deck.clicked.connect(self.generate_deck_action)
|
|
||||||
|
|
||||||
self.btn_generate_video = QPushButton("🎬 Generate Video")
|
|
||||||
self.btn_generate_video.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
self.btn_generate_video.setStyleSheet("""
|
|
||||||
QPushButton { background-color: #2980B9; color: white; font-weight: bold; font-size: 14px; padding: 10px 22px; border-radius: 5px; }
|
|
||||||
QPushButton:hover { background-color: #1F618D; }
|
|
||||||
""")
|
|
||||||
self.btn_generate_video.clicked.connect(self.generate_video_action)
|
|
||||||
|
|
||||||
bottom_layout.addWidget(self.btn_generate_deck)
|
|
||||||
bottom_layout.addWidget(self.btn_generate_video)
|
|
||||||
bottom_layout.addStretch()
|
|
||||||
|
|
||||||
main_layout.addLayout(bottom_layout)
|
|
||||||
|
|
||||||
# Populate initial states from backend
|
|
||||||
self.reload_review_pool()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def reload_review_pool(self):
|
|
||||||
"""Fetches consolidated text entries from the database and initializes cache."""
|
|
||||||
try:
|
|
||||||
self.all_cached_records = database.get_all_translations_explicit()
|
|
||||||
self.handle_live_filter()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error initializing flashcard review workspace: {e}")
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def handle_live_filter(self):
|
|
||||||
"""Filters grid contents and generates a randomized matching queue for the card engine."""
|
|
||||||
self.table.blockSignals(True)
|
|
||||||
self.table.setRowCount(0)
|
|
||||||
|
|
||||||
filter_ctx = self.txt_review_context.text().lower().strip()
|
|
||||||
filter_tag = self.txt_review_tags.text().lower().strip()
|
|
||||||
|
|
||||||
self.filtered_review_pool = []
|
|
||||||
visible_row_index = 0
|
|
||||||
|
|
||||||
for row in self.all_cached_records:
|
|
||||||
val_ctx = (row["source_context"] or "").lower()
|
|
||||||
val_tag = (row["tags"] or "").lower()
|
|
||||||
|
|
||||||
if (filter_ctx in val_ctx) and (filter_tag in val_tag):
|
|
||||||
self.filtered_review_pool.append(row)
|
|
||||||
|
|
||||||
self.table.insertRow(visible_row_index)
|
|
||||||
self.table.setItem(visible_row_index, 0, QTableWidgetItem(row["en_text"]))
|
|
||||||
self.table.setItem(visible_row_index, 1, QTableWidgetItem(row["es_text"]))
|
|
||||||
visible_row_index += 1
|
|
||||||
|
|
||||||
self.table.blockSignals(False)
|
|
||||||
|
|
||||||
# Reshuffle the active localized queue stack and reset card state tracking pointer
|
|
||||||
random.shuffle(self.filtered_review_pool)
|
|
||||||
self.current_index = 0 if self.filtered_review_pool else -1
|
|
||||||
self.is_flipped = False
|
|
||||||
self.display_current_card()
|
|
||||||
|
|
||||||
def display_current_card(self):
|
|
||||||
"""Pushes current pool row data configurations to layout containers."""
|
|
||||||
if not (0 <= self.current_index < len(self.filtered_review_pool)):
|
|
||||||
self.lbl_english.setText("No phrases match current active criteria filters.")
|
|
||||||
self.lbl_spanish.setText("")
|
|
||||||
self.card_stack.setCurrentIndex(0)
|
|
||||||
self.btn_flip_next.setText("Flip Card")
|
|
||||||
self.btn_flip_next.setEnabled(False)
|
|
||||||
self.btn_play_audio.setEnabled(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.btn_flip_next.setEnabled(True)
|
|
||||||
self.btn_play_audio.setEnabled(True)
|
|
||||||
record = self.filtered_review_pool[self.current_index]
|
|
||||||
|
|
||||||
# Setup front and back text labels
|
|
||||||
self.lbl_english.setText(record["en_text"])
|
|
||||||
self.lbl_spanish.setText(record["es_text"])
|
|
||||||
|
|
||||||
# Sync visual widget indexing configurations
|
|
||||||
if not self.is_flipped:
|
|
||||||
self.card_stack.setCurrentIndex(0)
|
|
||||||
self.btn_flip_next.setText("Flip Card")
|
|
||||||
else:
|
|
||||||
self.card_stack.setCurrentIndex(1)
|
|
||||||
self.btn_flip_next.setText("Next Card ➔")
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def handle_card_interaction(self):
|
|
||||||
"""State machine cycling through card flipped values or increments indices sequential steps."""
|
|
||||||
if not self.filtered_review_pool:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.is_flipped:
|
|
||||||
# Transition State: Front -> Back
|
|
||||||
self.is_flipped = True
|
|
||||||
self.display_current_card()
|
|
||||||
else:
|
|
||||||
# Transition State: Advance to next index item row
|
|
||||||
self.current_index += 1
|
|
||||||
if self.current_index >= len(self.filtered_review_pool):
|
|
||||||
self.current_index = 0
|
|
||||||
random.shuffle(self.filtered_review_pool) # Rescramble on completion pass loops
|
|
||||||
|
|
||||||
self.is_flipped = False
|
|
||||||
self.display_current_card()
|
|
||||||
|
|
||||||
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
|
|
||||||
try:
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
loop.run_until_complete(stream_audio())
|
|
||||||
loop.close()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[TTS Debug] Event loop error: {e}")
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def play_card_audio(self):
|
|
||||||
"""Auditions neural edge-tts voice 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)
|
|
||||||
|
|
||||||
if not self.is_flipped:
|
|
||||||
raw_text = record.get("en_text", "")
|
|
||||||
spoken_text = parse_text_for_edgetts(raw_text)
|
|
||||||
voice = "en-US-EmmaNeural"
|
|
||||||
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"
|
|
||||||
|
|
||||||
# Respect <meta sound-off> flags or empty entries
|
|
||||||
if not spoken_text.strip():
|
|
||||||
print("[TTS Debug] Skipped: Parsed text is empty or muted via sound-off tag.")
|
|
||||||
return
|
|
||||||
|
|
||||||
threading.Thread(
|
|
||||||
target=self._async_edge_speech_worker,
|
|
||||||
args=(spoken_text, voice, rate_string),
|
|
||||||
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."""
|
|
||||||
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():
|
|
||||||
deck_tree_parts.append(str(root_deck_name).strip())
|
|
||||||
|
|
||||||
if sub_deck_hierarchy and str(sub_deck_hierarchy).strip():
|
|
||||||
deck_tree_parts.append(str(sub_deck_hierarchy).strip())
|
|
||||||
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)
|
|
||||||
|
|
||||||
# --- 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(
|
|
||||||
self,
|
|
||||||
"Export Complete",
|
|
||||||
f"Successfully exported Anki package!\n\n"
|
|
||||||
f"<b>Deck Hierarchy:</b> {full_deck_namespace}\n"
|
|
||||||
f"<b>File Name:</b> {filename}\n"
|
|
||||||
f"<b>Path:</b> {file_path}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.critical(self, "Compiler Fault Safeguard", f"An exception occurred building your deck container package:\n{str(e)}")
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def generate_video_action(self):
|
|
||||||
"""Placeholder function execution trigger for media compiler production automation."""
|
|
||||||
QMessageBox.information(
|
|
||||||
self,
|
|
||||||
"Media Generator Active",
|
|
||||||
f"Initiating background video asset production using the {len(self.filtered_review_pool)} visible phrases."
|
|
||||||
)
|
|
||||||
|
|
@ -1,417 +0,0 @@
|
||||||
# tabs/sandbox_tab.py
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import asyncio
|
|
||||||
from PyQt6.QtWidgets import (
|
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
||||||
QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
|
|
||||||
QHeaderView, QMessageBox, QFormLayout, QDialog, QTextEdit,
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class TextEditorDialog(QDialog):
|
|
||||||
"""A pop-up modal containing a large text field workspace for copy-pasting extra text blocks or drafting HTML content."""
|
|
||||||
def __init__(self, title, initial_text="", parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle(title)
|
|
||||||
self.resize(650, 450)
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
|
|
||||||
info_label = QLabel("Edit text or raw HTML below (supports tables, lists, and inline styles):")
|
|
||||||
info_label.setStyleSheet("color: #7F8C8D; font-size: 12px;")
|
|
||||||
layout.addWidget(info_label)
|
|
||||||
|
|
||||||
self.editor = QTextEdit()
|
|
||||||
self.editor.setPlainText(initial_text)
|
|
||||||
# Monospaced font for clean HTML readability
|
|
||||||
self.editor.setStyleSheet("font-family: monospace; font-size: 13px; background-color: #2C3E50; color: #ECF0F1; padding: 8px;")
|
|
||||||
layout.addWidget(self.editor)
|
|
||||||
|
|
||||||
btn_layout = QHBoxLayout()
|
|
||||||
self.btn_save = QPushButton("Save / Apply")
|
|
||||||
self.btn_save.setStyleSheet("background-color: #27AE60; color: white; font-weight: bold; padding: 6px 14px;")
|
|
||||||
self.btn_save.clicked.connect(self.accept)
|
|
||||||
|
|
||||||
self.btn_cancel = QPushButton("Cancel")
|
|
||||||
self.btn_cancel.setStyleSheet("padding: 6px 14px;")
|
|
||||||
self.btn_cancel.clicked.connect(self.reject)
|
|
||||||
|
|
||||||
btn_layout.addStretch()
|
|
||||||
btn_layout.addWidget(self.btn_cancel)
|
|
||||||
btn_layout.addWidget(self.btn_save)
|
|
||||||
layout.addLayout(btn_layout)
|
|
||||||
|
|
||||||
def get_text(self):
|
|
||||||
return self.editor.toPlainText().strip()
|
|
||||||
|
|
||||||
|
|
||||||
class SandboxTab(QWidget):
|
|
||||||
data_mutated = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.selected_translation_id = None
|
|
||||||
|
|
||||||
# Local item memory caching for instant search lookups
|
|
||||||
self.cached_records = []
|
|
||||||
self.current_notes_content = ""
|
|
||||||
self.current_anki_notes_content = ""
|
|
||||||
|
|
||||||
main_layout = QVBoxLayout(self)
|
|
||||||
main_layout.setContentsMargins(30, 20, 30, 20)
|
|
||||||
main_layout.setSpacing(15)
|
|
||||||
|
|
||||||
# --- SECTION 1: FORM INPUT CRADLE ---
|
|
||||||
form_container = QWidget()
|
|
||||||
form_layout = QFormLayout(form_container)
|
|
||||||
form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
|
||||||
form_layout.setSpacing(10)
|
|
||||||
|
|
||||||
# --- English Input Row ---
|
|
||||||
english_widget = QWidget()
|
|
||||||
english_layout = QHBoxLayout(english_widget)
|
|
||||||
english_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
english_layout.setSpacing(8)
|
|
||||||
|
|
||||||
self.txt_english = QLineEdit()
|
|
||||||
self.txt_english.setPlaceholderText("Enter English phrase (filters grid real-time)...")
|
|
||||||
self.txt_english.setStyleSheet("padding: 6px; font-size: 14px;")
|
|
||||||
self.txt_english.textChanged.connect(self.apply_live_grid_filter)
|
|
||||||
|
|
||||||
self.btn_edit_english = QPushButton("✏️ Edit English Block")
|
|
||||||
self.btn_edit_english.setStyleSheet("padding: 6px 12px; font-weight: bold; background-color: #34495E; color: white; border-radius: 4px;")
|
|
||||||
self.btn_edit_english.clicked.connect(self.open_english_editor)
|
|
||||||
|
|
||||||
english_layout.addWidget(self.txt_english, stretch=1)
|
|
||||||
english_layout.addWidget(self.btn_edit_english, stretch=0)
|
|
||||||
|
|
||||||
# --- Spanish Input Row ---
|
|
||||||
spanish_widget = QWidget()
|
|
||||||
spanish_layout = QHBoxLayout(spanish_widget)
|
|
||||||
spanish_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
spanish_layout.setSpacing(8)
|
|
||||||
|
|
||||||
self.txt_spanish = QLineEdit()
|
|
||||||
self.txt_spanish.setPlaceholderText("Enter Spanish phrase (filters grid real-time)...")
|
|
||||||
self.txt_spanish.setStyleSheet("padding: 6px; font-size: 14px;")
|
|
||||||
self.txt_spanish.textChanged.connect(self.apply_live_grid_filter)
|
|
||||||
|
|
||||||
self.btn_edit_spanish = QPushButton("✏️ Edit Spanish Block")
|
|
||||||
self.btn_edit_spanish.setStyleSheet("padding: 6px 12px; font-weight: bold; background-color: #34495E; color: white; border-radius: 4px;")
|
|
||||||
self.btn_edit_spanish.clicked.connect(self.open_spanish_editor)
|
|
||||||
|
|
||||||
spanish_layout.addWidget(self.txt_spanish, stretch=1)
|
|
||||||
spanish_layout.addWidget(self.btn_edit_spanish, stretch=0)
|
|
||||||
|
|
||||||
self.txt_context = QLineEdit()
|
|
||||||
self.txt_context.setPlaceholderText("Context e.g., Camino 2027 (filters grid real-time)...")
|
|
||||||
self.txt_context.setStyleSheet("padding: 6px; font-size: 14px;")
|
|
||||||
self.txt_context.textChanged.connect(self.apply_live_grid_filter)
|
|
||||||
|
|
||||||
self.txt_tags = QLineEdit()
|
|
||||||
self.txt_tags.setPlaceholderText("Comma separated tags (filters grid real-time)...")
|
|
||||||
self.txt_tags.setStyleSheet("padding: 6px; font-size: 14px;")
|
|
||||||
self.txt_tags.textChanged.connect(self.apply_live_grid_filter)
|
|
||||||
|
|
||||||
# --- Sort Order Field (Added after Tags) ---
|
|
||||||
self.spn_sort_order = QSpinBox()
|
|
||||||
self.spn_sort_order.setRange(0, 999999)
|
|
||||||
self.spn_sort_order.setValue(0)
|
|
||||||
self.spn_sort_order.setStyleSheet("padding: 6px; font-size: 14px;")
|
|
||||||
|
|
||||||
# Modal Editor Row Buttons for Extended Notes
|
|
||||||
editor_buttons_layout = QHBoxLayout()
|
|
||||||
self.btn_edit_notes = QPushButton("📝 Edit Notes Block")
|
|
||||||
self.btn_edit_notes.clicked.connect(self.open_notes_editor)
|
|
||||||
|
|
||||||
self.btn_edit_anki_notes = QPushButton("🗂️ Edit Anki Notes Block")
|
|
||||||
self.btn_edit_anki_notes.clicked.connect(self.open_anki_notes_editor)
|
|
||||||
|
|
||||||
editor_buttons_layout.addWidget(self.btn_edit_notes)
|
|
||||||
editor_buttons_layout.addWidget(self.btn_edit_anki_notes)
|
|
||||||
|
|
||||||
# Add container widgets to QFormLayout rows
|
|
||||||
form_layout.addRow(QLabel("<b>English Phrase:</b>"), english_widget)
|
|
||||||
form_layout.addRow(QLabel("<b>Spanish Translation:</b>"), spanish_widget)
|
|
||||||
form_layout.addRow(QLabel("<b>Source Context:</b>"), self.txt_context)
|
|
||||||
form_layout.addRow(QLabel("<b>Tags:</b>"), self.txt_tags)
|
|
||||||
form_layout.addRow(QLabel("<b>Sort Order:</b>"), self.spn_sort_order)
|
|
||||||
form_layout.addRow(QLabel("<b>Extended Data Fields:</b>"), editor_buttons_layout)
|
|
||||||
|
|
||||||
main_layout.addWidget(form_container)
|
|
||||||
|
|
||||||
# --- INLINE AUDIO CONTROL + GENDER SELECTION PANEL ---
|
|
||||||
audio_panel = QHBoxLayout()
|
|
||||||
audio_panel.setSpacing(15)
|
|
||||||
|
|
||||||
self.btn_test_en = QPushButton("🔊 Test English Voice")
|
|
||||||
self.btn_test_en.clicked.connect(self.audition_english)
|
|
||||||
|
|
||||||
self.btn_test_es = QPushButton("🔊 Test Spanish Voice")
|
|
||||||
self.btn_test_es.clicked.connect(self.audition_spanish)
|
|
||||||
|
|
||||||
gender_label = QLabel("<b>Speaker Gender:</b>")
|
|
||||||
self.rb_female = QRadioButton("Female")
|
|
||||||
self.rb_male = QRadioButton("Male")
|
|
||||||
self.rb_female.setChecked(True)
|
|
||||||
|
|
||||||
self.gender_group = QButtonGroup(self)
|
|
||||||
self.gender_group.addButton(self.rb_female)
|
|
||||||
self.gender_group.addButton(self.rb_male)
|
|
||||||
|
|
||||||
audio_panel.addWidget(self.btn_test_en)
|
|
||||||
audio_panel.addWidget(self.btn_test_es)
|
|
||||||
audio_panel.addSpacing(20)
|
|
||||||
audio_panel.addWidget(gender_label)
|
|
||||||
audio_panel.addWidget(self.rb_female)
|
|
||||||
audio_panel.addWidget(self.rb_male)
|
|
||||||
audio_panel.addStretch()
|
|
||||||
|
|
||||||
main_layout.addLayout(audio_panel)
|
|
||||||
|
|
||||||
# --- ACTION CONTROL BAR ---
|
|
||||||
actions_layout = QHBoxLayout()
|
|
||||||
self.btn_save_record = QPushButton("📥 Save Transaction")
|
|
||||||
self.btn_save_record.clicked.connect(self.commit_form_entry)
|
|
||||||
self.btn_save_record.setStyleSheet("background-color: #27AE60; color: white; font-weight: bold; padding: 8px 16px;")
|
|
||||||
|
|
||||||
self.btn_clear_form = QPushButton("🧹 Reset Fields")
|
|
||||||
self.btn_clear_form.clicked.connect(self.clear_form_fields)
|
|
||||||
|
|
||||||
self.btn_delete_record = QPushButton("🗑️ Delete Selected")
|
|
||||||
self.btn_delete_record.clicked.connect(self.remove_target_record)
|
|
||||||
self.btn_delete_record.setStyleSheet("background-color: #C0392B; color: white;")
|
|
||||||
|
|
||||||
actions_layout.addWidget(self.btn_save_record)
|
|
||||||
actions_layout.addWidget(self.btn_clear_form)
|
|
||||||
actions_layout.addWidget(self.btn_delete_record)
|
|
||||||
actions_layout.addStretch()
|
|
||||||
main_layout.addLayout(actions_layout)
|
|
||||||
|
|
||||||
# --- VIEWPORT GRID TABLE ---
|
|
||||||
self.table = QTableWidget()
|
|
||||||
self.table.setColumnCount(7)
|
|
||||||
self.table.setHorizontalHeaderLabels(["ID", "English", "Spanish", "Context", "Tags", "Sort Order", "Gender"])
|
|
||||||
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
||||||
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
|
||||||
self.table.cellClicked.connect(self.populate_form_from_grid)
|
|
||||||
|
|
||||||
header = self.table.horizontalHeader()
|
|
||||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
|
||||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
||||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
|
||||||
header.setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
|
|
||||||
|
|
||||||
main_layout.addWidget(self.table)
|
|
||||||
self.reload_table_display()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def open_english_editor(self):
|
|
||||||
dlg = TextEditorDialog("Edit English Phrase / HTML Block", self.txt_english.text(), self)
|
|
||||||
if dlg.exec():
|
|
||||||
self.txt_english.setText(dlg.get_text())
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def open_spanish_editor(self):
|
|
||||||
dlg = TextEditorDialog("Edit Spanish Translation / HTML Block", self.txt_spanish.text(), self)
|
|
||||||
if dlg.exec():
|
|
||||||
self.txt_spanish.setText(dlg.get_text())
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def open_notes_editor(self):
|
|
||||||
dlg = TextEditorDialog("Edit Grammar / Core Notes Block", self.current_notes_content, self)
|
|
||||||
if dlg.exec():
|
|
||||||
self.current_notes_content = dlg.get_text()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def open_anki_notes_editor(self):
|
|
||||||
dlg = TextEditorDialog("Edit Anki Specialized Meta Field", self.current_anki_notes_content, self)
|
|
||||||
if dlg.exec():
|
|
||||||
self.current_anki_notes_content = dlg.get_text()
|
|
||||||
|
|
||||||
def clear_form_fields(self):
|
|
||||||
self.txt_english.blockSignals(True)
|
|
||||||
self.txt_spanish.blockSignals(True)
|
|
||||||
self.txt_context.blockSignals(True)
|
|
||||||
self.txt_tags.blockSignals(True)
|
|
||||||
|
|
||||||
self.selected_translation_id = None
|
|
||||||
self.txt_english.clear()
|
|
||||||
self.txt_spanish.clear()
|
|
||||||
self.txt_context.clear()
|
|
||||||
self.txt_tags.clear()
|
|
||||||
self.spn_sort_order.setValue(0)
|
|
||||||
self.current_notes_content = ""
|
|
||||||
self.current_anki_notes_content = ""
|
|
||||||
self.rb_female.setChecked(True)
|
|
||||||
|
|
||||||
self.txt_english.blockSignals(False)
|
|
||||||
self.txt_spanish.blockSignals(False)
|
|
||||||
self.txt_context.blockSignals(False)
|
|
||||||
self.txt_tags.blockSignals(False)
|
|
||||||
|
|
||||||
self.apply_live_grid_filter()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def commit_form_entry(self):
|
|
||||||
en_t = self.txt_english.text().strip()
|
|
||||||
es_t = self.txt_spanish.text().strip()
|
|
||||||
ctx_t = self.txt_context.text().strip()
|
|
||||||
tag_t = self.txt_tags.text().strip()
|
|
||||||
sort_val = self.spn_sort_order.value()
|
|
||||||
gender_t = "Male" if self.rb_male.isChecked() else "Female"
|
|
||||||
|
|
||||||
if not en_t or not es_t:
|
|
||||||
QMessageBox.warning(self, "Validation Alert", "English and Spanish phrase properties cannot remain blank.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.selected_translation_id is None:
|
|
||||||
database.insert_translation_record(
|
|
||||||
es_t, en_t, ctx_t, tag_t,
|
|
||||||
self.current_notes_content, gender_t,
|
|
||||||
self.current_anki_notes_content, sort_val
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
database.update_translation_record(
|
|
||||||
self.selected_translation_id, es_t, en_t, ctx_t, tag_t,
|
|
||||||
self.current_notes_content, gender_t,
|
|
||||||
self.current_anki_notes_content, sort_val
|
|
||||||
)
|
|
||||||
|
|
||||||
self.clear_form_fields()
|
|
||||||
self.reload_table_display()
|
|
||||||
self.data_mutated.emit()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def remove_target_record(self):
|
|
||||||
if self.selected_translation_id is None:
|
|
||||||
QMessageBox.warning(self, "Selection Missing", "Please select a row from the grid viewport before attempting deletion.")
|
|
||||||
return
|
|
||||||
|
|
||||||
confirm = QMessageBox.question(
|
|
||||||
self,
|
|
||||||
"Confirm Deletion",
|
|
||||||
"Are you sure you want to permanently delete this translation record?",
|
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
||||||
)
|
|
||||||
|
|
||||||
if confirm == QMessageBox.StandardButton.Yes:
|
|
||||||
database.delete_translation_record(self.selected_translation_id)
|
|
||||||
self.clear_form_fields()
|
|
||||||
self.reload_table_display()
|
|
||||||
self.data_mutated.emit()
|
|
||||||
|
|
||||||
def populate_form_from_grid(self, row, col):
|
|
||||||
self.selected_translation_id = int(self.table.item(row, 0).text())
|
|
||||||
record = database.get_translation_by_id(self.selected_translation_id)
|
|
||||||
|
|
||||||
if record:
|
|
||||||
self.txt_english.blockSignals(True)
|
|
||||||
self.txt_spanish.blockSignals(True)
|
|
||||||
self.txt_context.blockSignals(True)
|
|
||||||
self.txt_tags.blockSignals(True)
|
|
||||||
|
|
||||||
self.txt_english.setText(record["en_text"])
|
|
||||||
self.txt_spanish.setText(record["es_text"])
|
|
||||||
self.txt_context.setText(record.get("source_context", ""))
|
|
||||||
self.txt_tags.setText(record.get("tags", ""))
|
|
||||||
self.spn_sort_order.setValue(record.get("sort_order", 0))
|
|
||||||
self.current_notes_content = record.get("notes", "")
|
|
||||||
self.current_anki_notes_content = record.get("anki_notes", "")
|
|
||||||
|
|
||||||
if record.get("gender") == "Male":
|
|
||||||
self.rb_male.setChecked(True)
|
|
||||||
else:
|
|
||||||
self.rb_female.setChecked(True)
|
|
||||||
|
|
||||||
self.txt_english.blockSignals(False)
|
|
||||||
self.txt_spanish.blockSignals(False)
|
|
||||||
self.txt_context.blockSignals(False)
|
|
||||||
self.txt_tags.blockSignals(False)
|
|
||||||
|
|
||||||
def reload_table_display(self):
|
|
||||||
self.cached_records = database.get_all_translations_explicit()
|
|
||||||
self.apply_live_grid_filter()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def apply_live_grid_filter(self):
|
|
||||||
filter_en = self.txt_english.text().lower().strip()
|
|
||||||
filter_es = self.txt_spanish.text().lower().strip()
|
|
||||||
filter_ctx = self.txt_context.text().lower().strip()
|
|
||||||
filter_tags = self.txt_tags.text().lower().strip()
|
|
||||||
|
|
||||||
self.table.setRowCount(0)
|
|
||||||
visible_row_idx = 0
|
|
||||||
|
|
||||||
for r in self.cached_records:
|
|
||||||
match_en = filter_en in (r.get("en_text") or "").lower()
|
|
||||||
match_es = filter_es in (r.get("es_text") or "").lower()
|
|
||||||
match_ctx = filter_ctx in (r.get("source_context") or "").lower()
|
|
||||||
match_tags = filter_tags in (r.get("tags") or "").lower()
|
|
||||||
|
|
||||||
if match_en and match_es and match_ctx and match_tags:
|
|
||||||
self.table.insertRow(visible_row_idx)
|
|
||||||
self.table.setItem(visible_row_idx, 0, QTableWidgetItem(str(r["translation_id"])))
|
|
||||||
self.table.setItem(visible_row_idx, 1, QTableWidgetItem(r["en_text"]))
|
|
||||||
self.table.setItem(visible_row_idx, 2, QTableWidgetItem(r["es_text"]))
|
|
||||||
self.table.setItem(visible_row_idx, 3, QTableWidgetItem(r.get("source_context", "")))
|
|
||||||
self.table.setItem(visible_row_idx, 4, QTableWidgetItem(r.get("tags", "")))
|
|
||||||
self.table.setItem(visible_row_idx, 5, QTableWidgetItem(str(r.get("sort_order", 0))))
|
|
||||||
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())
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def audition_english(self):
|
|
||||||
raw_txt = self.txt_english.text().strip()
|
|
||||||
spoken_text = parse_text_for_edgetts(raw_txt)
|
|
||||||
|
|
||||||
if not spoken_text.strip():
|
|
||||||
return
|
|
||||||
|
|
||||||
settings = database.load_all_settings() or {}
|
|
||||||
rate_string = get_configured_tts_rate(settings)
|
|
||||||
|
|
||||||
threading.Thread(
|
|
||||||
target=self._async_edge_speech_worker,
|
|
||||||
args=(spoken_text, "en-US-EmmaNeural", rate_string),
|
|
||||||
daemon=True
|
|
||||||
).start()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def audition_spanish(self):
|
|
||||||
raw_txt = self.txt_spanish.text().strip()
|
|
||||||
spoken_text = parse_text_for_edgetts(raw_txt)
|
|
||||||
|
|
||||||
if not spoken_text.strip():
|
|
||||||
return
|
|
||||||
|
|
||||||
voice = "es-ES-AlvaroNeural" if self.rb_male.isChecked() else "es-ES-ElviraNeural"
|
|
||||||
settings = database.load_all_settings() or {}
|
|
||||||
rate_string = get_configured_tts_rate(settings)
|
|
||||||
|
|
||||||
threading.Thread(
|
|
||||||
target=self._async_edge_speech_worker,
|
|
||||||
args=(spoken_text, voice, rate_string),
|
|
||||||
daemon=True
|
|
||||||
).start()
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
# tabs/settings_tab.py
|
|
||||||
import os
|
|
||||||
from PyQt6.QtWidgets import (
|
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
|
||||||
QPushButton, QFileDialog, QGroupBox, QFormLayout,
|
|
||||||
QMessageBox, QFrame
|
|
||||||
)
|
|
||||||
from PyQt6.QtCore import pyqtSignal, pyqtSlot
|
|
||||||
import database
|
|
||||||
|
|
||||||
class SettingsTab(QWidget):
|
|
||||||
# Signals to communicate up to the centralized main.py loop coordinator
|
|
||||||
settings_changed = pyqtSignal(str, str) # Emits: (key, value)
|
|
||||||
export_anki_requested = pyqtSignal(str) # Emits: (full_deck_name)
|
|
||||||
generate_video_requested = pyqtSignal() # Emits: trigger
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
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")
|
|
||||||
anki_form = QFormLayout(anki_group)
|
|
||||||
anki_form.setSpacing(10)
|
|
||||||
|
|
||||||
self.txt_root_deck = QLineEdit()
|
|
||||||
self.txt_root_deck.setPlaceholderText("e.g., Spanish::CAE_Course")
|
|
||||||
self.txt_root_deck.textChanged.connect(lambda text: self.update_setting("anki_root_deck_name", text.strip()))
|
|
||||||
|
|
||||||
self.txt_sub_deck = QLineEdit()
|
|
||||||
self.txt_sub_deck.setPlaceholderText("e.g., Vocabulary::Unit_1")
|
|
||||||
self.txt_sub_deck.textChanged.connect(lambda text: self.update_setting("anki_sub_deck_name", text.strip()))
|
|
||||||
|
|
||||||
# Export Destination Directory Picker
|
|
||||||
dir_picker_layout = QHBoxLayout()
|
|
||||||
self.txt_export_dir = QLineEdit()
|
|
||||||
self.txt_export_dir.setReadOnly(True)
|
|
||||||
self.txt_export_dir.setStyleSheet("background-color: #F8F9F9; color: #34495E;")
|
|
||||||
|
|
||||||
btn_browse = QPushButton("Browse...")
|
|
||||||
btn_browse.clicked.connect(self.browse_export_directory)
|
|
||||||
dir_picker_layout.addWidget(self.txt_export_dir)
|
|
||||||
dir_picker_layout.addWidget(btn_browse)
|
|
||||||
|
|
||||||
anki_form.addRow("Root Deck Name:", self.txt_root_deck)
|
|
||||||
anki_form.addRow("Sub-Deck Namespace Hierarchy:", self.txt_sub_deck)
|
|
||||||
anki_form.addRow("Export Target Directory:", dir_picker_layout)
|
|
||||||
|
|
||||||
main_layout.addWidget(anki_group)
|
|
||||||
|
|
||||||
# --- SECTION 2: AUDIO ENGINE & COMPILATION OVERRIDES ---
|
|
||||||
engine_group = QGroupBox("Voice Synthesis & Training Configuration")
|
|
||||||
engine_form = QFormLayout(engine_group)
|
|
||||||
|
|
||||||
self.txt_tts_voice = QLineEdit()
|
|
||||||
self.txt_tts_voice.setPlaceholderText("Apple_Monica")
|
|
||||||
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("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)
|
|
||||||
engine_form.addRow("Target Speech Playback Multiplier:", self.txt_tts_speed)
|
|
||||||
|
|
||||||
main_layout.addWidget(engine_group)
|
|
||||||
|
|
||||||
# Decorative divider line
|
|
||||||
divider = QFrame()
|
|
||||||
divider.setFrameShape(QFrame.Shape.HLine)
|
|
||||||
divider.setFrameShadow(QFrame.Shadow.Sunken)
|
|
||||||
main_layout.addWidget(divider)
|
|
||||||
|
|
||||||
# --- SECTION 3: SYSTEM ACTION EXECUTION BAR ---
|
|
||||||
actions_group = QGroupBox("Execution Pipelines")
|
|
||||||
actions_layout = QHBoxLayout(actions_group)
|
|
||||||
actions_layout.setSpacing(20)
|
|
||||||
|
|
||||||
self.btn_export_anki = QPushButton("🚀 Compile Lightweight Anki APKG")
|
|
||||||
self.btn_export_anki.setStyleSheet("""
|
|
||||||
QPushButton { background-color: #2980B9; color: white; font-weight: bold; font-size: 13px; padding: 10px; border-radius: 4px; }
|
|
||||||
QPushButton:hover { background-color: #3498DB; }
|
|
||||||
""")
|
|
||||||
self.btn_export_anki.clicked.connect(self.dispatch_anki_export)
|
|
||||||
|
|
||||||
self.btn_gen_video = QPushButton("🎬 Generate MP4 Loop Playlists")
|
|
||||||
self.btn_gen_video.setStyleSheet("""
|
|
||||||
QPushButton { background-color: #8E44AD; color: white; font-weight: bold; font-size: 13px; padding: 10px; border-radius: 4px; }
|
|
||||||
QPushButton:hover { background-color: #9B59B6; }
|
|
||||||
""")
|
|
||||||
self.btn_gen_video.clicked.connect(self.generate_video_requested.emit)
|
|
||||||
|
|
||||||
actions_layout.addWidget(self.btn_export_anki)
|
|
||||||
actions_layout.addWidget(self.btn_gen_video)
|
|
||||||
|
|
||||||
main_layout.addWidget(actions_group)
|
|
||||||
main_layout.addStretch() # Push everything neatly to the top
|
|
||||||
|
|
||||||
# Load settings from database onto inputs on initialization
|
|
||||||
self.populate_fields_from_db_state()
|
|
||||||
|
|
||||||
def populate_fields_from_db_state(self):
|
|
||||||
"""Fetches stored parameters on view load initialization."""
|
|
||||||
# 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", "0.75"))
|
|
||||||
|
|
||||||
self.blockSignals(False)
|
|
||||||
|
|
||||||
def update_setting(self, key, value):
|
|
||||||
"""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("~")
|
|
||||||
selected_directory = QFileDialog.getExistingDirectory(
|
|
||||||
self, "Select Anki Export Target Location", current_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
if selected_directory:
|
|
||||||
self.txt_export_dir.setText(selected_directory)
|
|
||||||
self.update_setting("anki_export_directory", selected_directory)
|
|
||||||
|
|
||||||
def dispatch_anki_export(self):
|
|
||||||
"""Constructs and validates the structured deck names namespace before signaling main.py."""
|
|
||||||
root = self.txt_root_deck.text().strip()
|
|
||||||
sub = self.txt_sub_deck.text().strip()
|
|
||||||
|
|
||||||
if not root:
|
|
||||||
QMessageBox.warning(self, "Invalid Parameters", "A root deck namespace destination must be provided.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Combine hierarchy into standard Anki format: 'Root::SubDeck'
|
|
||||||
full_deck_name = f"{root}::{sub}" if sub else root
|
|
||||||
self.export_anki_requested.emit(full_deck_name)
|
|
||||||
69
test_tts.py
|
|
@ -1,55 +1,28 @@
|
||||||
# tts_utils.py
|
import asyncio
|
||||||
import re
|
import os
|
||||||
from bs4 import BeautifulSoup
|
import edge_tts
|
||||||
|
|
||||||
|
# 1. Define the phrase, output path, and target Castellano voice
|
||||||
|
SPANISH_TEXT = "¡Buenos días! ¿Cómo estás? Bienvenido a tu curso de español."
|
||||||
|
OUTPUT_FILE = "media/buenos_dias_castellano-Female.mp3"
|
||||||
|
#VOICE = "es-ES-AlvaroNeural" # Swap to "es-ES-ElviraNeural" if you prefer a female tone
|
||||||
|
VOICE = "es-ES-ElviraNeural" # Swap to "es-ES-ElviraNeural" if you prefer a female tone
|
||||||
|
|
||||||
def get_configured_tts_rate(settings: dict) -> str:
|
async def generate_castilian_audio():
|
||||||
"""Converts a numerical speed multiplier (e.g., 0.75, 1.0, 1.25) from app settings
|
# Ensure our target media folder exists locally
|
||||||
into Edge TTS percentage format string (e.g., '-25%', '+0%', '+25%').
|
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
|
||||||
"""
|
|
||||||
# Check the actual database key 'tts_playback_speed' with fallbacks
|
|
||||||
raw_val = (
|
|
||||||
settings.get("tts_playback_speed")
|
|
||||||
or settings.get("tts_speed_multiplier")
|
|
||||||
or 1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
print(f"🔄 Synthesizing text using Castilian voice: {VOICE}...")
|
||||||
raw_rate = float(raw_val)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
raw_rate = 1.0
|
|
||||||
|
|
||||||
# Calculate percentage shift relative to baseline 1.0
|
# 2. Configure the Communicate engine
|
||||||
pct = int(round((raw_rate - 1.0) * 100))
|
communicate = edge_tts.Communicate(SPANISH_TEXT, VOICE)
|
||||||
if pct >= 0:
|
|
||||||
return f"+{pct}%"
|
|
||||||
return f"{pct}%"
|
|
||||||
|
|
||||||
|
# 3. Stream and write the data packets to disk
|
||||||
|
await communicate.save(OUTPUT_FILE)
|
||||||
|
|
||||||
def parse_text_for_edgetts(html_content: str) -> str:
|
print(f"✨ Success! MP3 file exported safely to: {OUTPUT_FILE}")
|
||||||
"""Strips content between <meta sound-off> and <meta sound-on> tags,
|
print(f"📂 File size: {os.path.getsize(OUTPUT_FILE)} bytes")
|
||||||
handling optional whitespace inside the tag brackets (e.g., <meta sound-off >).
|
|
||||||
Converts remaining HTML tags to clean spoken text.
|
|
||||||
"""
|
|
||||||
if not html_content:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Flexible regex to slice out everything from <meta sound-off ...> through <meta sound-on ...>
|
if __name__ == "__main__":
|
||||||
pattern = re.compile(
|
# Run the async loop loop natively
|
||||||
r"<meta\s+sound-off\s*\/?>.*?<meta\s+sound-on\s*\/?>",
|
asyncio.run(generate_castilian_audio())
|
||||||
re.DOTALL | re.IGNORECASE,
|
|
||||||
)
|
|
||||||
cleaned_html = re.sub(pattern, "", html_content)
|
|
||||||
|
|
||||||
# Handle unclosed <meta sound-off> (mute rest of string from that point)
|
|
||||||
if re.search(r"<meta\s+sound-off\s*\/?>", cleaned_html, re.IGNORECASE):
|
|
||||||
cleaned_html = re.split(
|
|
||||||
r"<meta\s+sound-off\s*\/?>", cleaned_html, flags=re.IGNORECASE
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
# Convert remaining HTML into plain text for speech
|
|
||||||
soup = BeautifulSoup(cleaned_html, "html.parser")
|
|
||||||
text = soup.get_text(separator=" ")
|
|
||||||
|
|
||||||
# Normalize extra whitespace
|
|
||||||
return re.sub(r"\s+", " ", text).strip()
|
|
||||||
981
tree.txt
|
|
@ -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
|
|
||||||
69
tree2.txt
|
|
@ -1,69 +0,0 @@
|
||||||
.
|
|
||||||
├── 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
|
|
||||||
52
tts_utils.py
|
|
@ -1,52 +0,0 @@
|
||||||
# tts_utils.py
|
|
||||||
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'
|
|
||||||
raw_val = settings.get("tts_playback_speed", 1.0) if settings else 1.0
|
|
||||||
|
|
||||||
try:
|
|
||||||
raw_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}%"
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"[TTS Debug] DB Key 'tts_playback_speed': {raw_val} -> Formatted Rate: {rate_str}"
|
|
||||||
)
|
|
||||||
return rate_str
|
|
||||||
|
|
||||||
|
|
||||||
def parse_text_for_edgetts(html_content: str) -> str:
|
|
||||||
"""Strips content between <meta sound-off> and <meta sound-on> tags,
|
|
||||||
handling optional whitespace inside the tag brackets (e.g., <meta sound-off >).
|
|
||||||
Converts remaining HTML tags to clean spoken text.
|
|
||||||
"""
|
|
||||||
if not html_content:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Flexible regex to slice out everything from <meta sound-off ...> through <meta sound-on ...>
|
|
||||||
pattern = re.compile(
|
|
||||||
r"<meta\s+sound-off\s*\/?>.*?<meta\s+sound-on\s*\/?>",
|
|
||||||
re.DOTALL | re.IGNORECASE,
|
|
||||||
)
|
|
||||||
cleaned_html = re.sub(pattern, "", html_content)
|
|
||||||
|
|
||||||
# Handle unclosed <meta sound-off> (mute rest of string from that point)
|
|
||||||
if re.search(r"<meta\s+sound-off\s*\/?>", cleaned_html, re.IGNORECASE):
|
|
||||||
cleaned_html = re.split(
|
|
||||||
r"<meta\s+sound-off\s*\/?>", cleaned_html, flags=re.IGNORECASE
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
# Convert remaining HTML into plain text for speech
|
|
||||||
soup = BeautifulSoup(cleaned_html, "html.parser")
|
|
||||||
text = soup.get_text(separator=" ")
|
|
||||||
|
|
||||||
# Normalize extra whitespace
|
|
||||||
return re.sub(r"\s+", " ", text).strip()
|
|
||||||
81
uv.lock
|
|
@ -24,7 +24,6 @@ dependencies = [
|
||||||
{ name = "librosa" },
|
{ name = "librosa" },
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "pyinstaller" },
|
|
||||||
{ name = "pyqt6" },
|
{ name = "pyqt6" },
|
||||||
{ name = "scipy" },
|
{ name = "scipy" },
|
||||||
{ name = "sounddevice" },
|
{ name = "sounddevice" },
|
||||||
|
|
@ -39,7 +38,6 @@ requires-dist = [
|
||||||
{ name = "librosa", specifier = ">=0.11.0" },
|
{ name = "librosa", specifier = ">=0.11.0" },
|
||||||
{ name = "numpy", specifier = ">=2.4.6" },
|
{ name = "numpy", specifier = ">=2.4.6" },
|
||||||
{ name = "pillow", specifier = ">=12.2.0" },
|
{ name = "pillow", specifier = ">=12.2.0" },
|
||||||
{ name = "pyinstaller", specifier = ">=6.21.0" },
|
|
||||||
{ name = "pyqt6", specifier = ">=6.11.0" },
|
{ name = "pyqt6", specifier = ">=6.11.0" },
|
||||||
{ name = "scipy", specifier = ">=1.17.1" },
|
{ name = "scipy", specifier = ">=1.17.1" },
|
||||||
{ name = "sounddevice", specifier = ">=0.5.5" },
|
{ name = "sounddevice", specifier = ">=0.5.5" },
|
||||||
|
|
@ -165,15 +163,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "annotated-doc"
|
name = "annotated-doc"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
|
|
@ -1163,18 +1152,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "mail-parser"
|
name = "mail-parser"
|
||||||
version = "4.4.0"
|
version = "4.4.0"
|
||||||
|
|
@ -1767,15 +1744,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "12.2.0"
|
version = "12.2.0"
|
||||||
|
|
@ -2111,46 +2079,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pylatexenc"
|
name = "pylatexenc"
|
||||||
version = "2.10"
|
version = "2.10"
|
||||||
|
|
@ -2299,15 +2227,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
|
|
|
||||||