2026-06-12 11:58:47 +00:00
|
|
|
|
# main.py
|
2026-06-14 09:56:43 +00:00
|
|
|
|
import sys
|
|
|
|
|
|
import os
|
2026-06-19 05:30:59 +00:00
|
|
|
|
import random
|
|
|
|
|
|
import hashlib
|
2026-06-14 09:56:43 +00:00
|
|
|
|
from PyQt6.QtWidgets import (
|
|
|
|
|
|
QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout,
|
|
|
|
|
|
QHBoxLayout, QLabel, QPushButton, QLineEdit, QComboBox,
|
2026-06-19 05:09:05 +00:00
|
|
|
|
QTableWidget, QTableWidgetItem, QSlider, QFormLayout, QTextEdit, QFrame, QMessageBox, QFileDialog
|
2026-06-14 09:56:43 +00:00
|
|
|
|
)
|
|
|
|
|
|
from PyQt6.QtCore import Qt, QUrl
|
|
|
|
|
|
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
|
|
|
|
|
|
from PyQt6.QtGui import QFont
|
2026-06-16 05:06:08 +00:00
|
|
|
|
|
2026-06-19 05:30:59 +00:00
|
|
|
|
# Third-Party Anki Generation Tooling
|
|
|
|
|
|
import genanki
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
# Internal Project Module Imports
|
2026-06-14 09:56:43 +00:00
|
|
|
|
from database.connection import init_db, get_connection
|
2026-06-13 07:02:06 +00:00
|
|
|
|
from core.bulk_importer import BulkImporter
|
2026-06-13 09:59:02 +00:00
|
|
|
|
from core.clean_glossary import GlossaryCleaner
|
2026-06-12 11:58:47 +00:00
|
|
|
|
|
2026-06-14 09:56:43 +00:00
|
|
|
|
class SpanishTrainerApp(QMainWindow):
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.setWindowTitle("Castilian Voice Trainer Pro")
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.setMinimumSize(1200, 750)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
# 1. Initialize schema structures and check ingestion status
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.ensure_database_populated()
|
|
|
|
|
|
|
2026-06-19 05:09:05 +00:00
|
|
|
|
# Load system persistent settings from DB
|
|
|
|
|
|
self.load_system_settings()
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
# Audio Player Architecture Setup
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.media_player = QMediaPlayer()
|
|
|
|
|
|
self.audio_output = QAudioOutput()
|
|
|
|
|
|
self.media_player.setAudioOutput(self.audio_output)
|
2026-06-19 03:42:28 +00:00
|
|
|
|
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.current_flashcard_id = None
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.flashcard_ids_pool = [] # Tracks currently filtered study list IDs
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
# Central Main Window Tabs Interface
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.tabs = QTabWidget()
|
|
|
|
|
|
self.setCentralWidget(self.tabs)
|
|
|
|
|
|
|
|
|
|
|
|
self.init_phrase_sandbox_tab()
|
|
|
|
|
|
self.init_flashcard_reviewer_tab()
|
2026-06-19 05:30:59 +00:00
|
|
|
|
self.init_settings_tab()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-19 05:09:05 +00:00
|
|
|
|
# 2. Populate table grids on initialization
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.refresh_crud_table()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.refresh_review_table()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
def ensure_database_populated(self):
|
2026-06-16 05:06:08 +00:00
|
|
|
|
"""Forces database configuration structure and triggers pipeline execution if empty."""
|
|
|
|
|
|
print("🗄️ Verification Pass: Running schema configuration scripts...")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
init_db()
|
|
|
|
|
|
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
|
2026-06-19 05:09:05 +00:00
|
|
|
|
cursor.execute("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT);")
|
|
|
|
|
|
|
2026-06-14 09:56:43 +00:00
|
|
|
|
try:
|
2026-06-16 05:06:08 +00:00
|
|
|
|
cursor.execute("SELECT COUNT(*) FROM translations")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
count = cursor.fetchone()[0]
|
2026-06-16 05:06:08 +00:00
|
|
|
|
print(f"📊 Current Translation Pairs found in database: {count}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"⚠️ Table check encountered an issue (likely empty tables): {e}")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
count = 0
|
2026-06-16 05:06:08 +00:00
|
|
|
|
finally:
|
|
|
|
|
|
conn.close()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
if count == 0:
|
2026-06-16 05:06:08 +00:00
|
|
|
|
print("🗄️ Database tables are empty. Triggering glossary reader pipeline...")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
pdf_file = "aula_int_plus_1_glos_en_alfa.pdf"
|
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists(pdf_file):
|
|
|
|
|
|
importer = BulkImporter()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
importer.import_pdf_glossary(pdf_file, "Aula Internacional Plus 1")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
cleaner = GlossaryCleaner()
|
|
|
|
|
|
cleaner.process_database_clean()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
print("✨ Ingestion pipeline processing sequence successfully completed.")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
else:
|
2026-06-16 05:06:08 +00:00
|
|
|
|
print(f"❌ Error: Source document '{pdf_file}' is missing from the directory root.")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-19 05:09:05 +00:00
|
|
|
|
def load_system_settings(self):
|
|
|
|
|
|
"""Loads persistent path directories from the key-value settings table."""
|
|
|
|
|
|
self.anki_export_dir = os.getcwd()
|
|
|
|
|
|
self.video_export_dir = os.getcwd()
|
|
|
|
|
|
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
try:
|
|
|
|
|
|
cursor.execute("SELECT key, value FROM settings")
|
|
|
|
|
|
rows = cursor.fetchall()
|
|
|
|
|
|
for row in rows:
|
|
|
|
|
|
if row[0] == "anki_export_directory":
|
|
|
|
|
|
self.anki_export_dir = row[1]
|
|
|
|
|
|
elif row[0] == "video_export_directory":
|
|
|
|
|
|
self.video_export_dir = row[1]
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"⚠️ Failed to read application settings from database: {e}")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
def save_setting_to_db(self, key, value):
|
|
|
|
|
|
"""Updates or inserts a specific system runtime variable into the database."""
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
try:
|
|
|
|
|
|
cursor.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", (key, value))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"❌ Critical: Failed to save setting '{key}': {e}")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
2026-06-14 09:56:43 +00:00
|
|
|
|
# =====================================================================
|
2026-06-16 05:06:08 +00:00
|
|
|
|
# 🗄️ TAB 1: TRANSLATION-CENTRIC PHRASE SANDBOX (CRUD)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
# =====================================================================
|
|
|
|
|
|
def init_phrase_sandbox_tab(self):
|
|
|
|
|
|
tab = QWidget()
|
|
|
|
|
|
layout = QHBoxLayout(tab)
|
|
|
|
|
|
|
|
|
|
|
|
left_panel = QVBoxLayout()
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
filter_layout = QHBoxLayout()
|
|
|
|
|
|
filter_layout.addWidget(QLabel("🔍 Text Filter:"))
|
|
|
|
|
|
self.search_text_input = QLineEdit()
|
|
|
|
|
|
self.search_text_input.setPlaceholderText("Search Spanish or English text blocks...")
|
|
|
|
|
|
self.search_text_input.textChanged.connect(self.refresh_crud_table)
|
|
|
|
|
|
filter_layout.addWidget(self.search_text_input)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
filter_layout.addWidget(QLabel("📂 Context:"))
|
|
|
|
|
|
self.search_context_input = QLineEdit()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.search_context_input.setPlaceholderText("e.g. U2")
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.search_context_input.setMaximumWidth(130)
|
|
|
|
|
|
self.search_context_input.textChanged.connect(self.refresh_crud_table)
|
|
|
|
|
|
filter_layout.addWidget(self.search_context_input)
|
|
|
|
|
|
|
|
|
|
|
|
left_panel.addLayout(filter_layout)
|
|
|
|
|
|
|
|
|
|
|
|
self.translation_table = QTableWidget()
|
|
|
|
|
|
self.translation_table.setColumnCount(6)
|
|
|
|
|
|
self.translation_table.setHorizontalHeaderLabels([
|
|
|
|
|
|
"TX ID", "Spanish Phrase", "English Translation", "Type", "Source Context", "Deck Assignment"
|
|
|
|
|
|
])
|
|
|
|
|
|
self.translation_table.itemSelectionChanged.connect(self.handle_table_row_select)
|
|
|
|
|
|
left_panel.addWidget(self.translation_table)
|
|
|
|
|
|
|
|
|
|
|
|
nav_layout = QHBoxLayout()
|
|
|
|
|
|
self.btn_row_up = QPushButton("🔼 Previous Pair")
|
|
|
|
|
|
self.btn_row_down = QPushButton("🔽 Next Pair")
|
|
|
|
|
|
self.btn_row_up.clicked.connect(lambda: self.step_table_row(-1))
|
|
|
|
|
|
self.btn_row_down.clicked.connect(lambda: self.step_table_row(1))
|
|
|
|
|
|
nav_layout.addWidget(self.btn_row_up)
|
|
|
|
|
|
nav_layout.addWidget(self.btn_row_down)
|
|
|
|
|
|
left_panel.addLayout(nav_layout)
|
|
|
|
|
|
|
2026-06-14 09:56:43 +00:00
|
|
|
|
right_panel = QVBoxLayout()
|
|
|
|
|
|
form_frame = QFrame()
|
|
|
|
|
|
form_frame.setFrameShape(QFrame.Shape.StyledPanel)
|
|
|
|
|
|
form_layout = QFormLayout(form_frame)
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.input_tx_id = QLineEdit()
|
|
|
|
|
|
self.input_tx_id.setReadOnly(True)
|
|
|
|
|
|
self.input_tx_id.setPlaceholderText("Auto-Increment ID")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.input_text_es = QTextEdit()
|
|
|
|
|
|
self.input_text_es.setMaximumHeight(75)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.input_text_en = QTextEdit()
|
|
|
|
|
|
self.input_text_en.setMaximumHeight(75)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
self.combo_type = QComboBox()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.combo_type.addItems(["phrase", "sentence", "noun", "verb", "adjective"])
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.input_context = QLineEdit()
|
|
|
|
|
|
self.input_context.setPlaceholderText("e.g., U8_5A")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-18 12:47:29 +00:00
|
|
|
|
self.input_tags = QLineEdit()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.input_tags.setPlaceholderText("e.g., irregular_er boots_verb")
|
2026-06-18 12:47:29 +00:00
|
|
|
|
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.input_deck_tag = QLineEdit()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.input_deck_tag.setPlaceholderText("Anki Sub-deck Hierarchy")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
button_qss = """
|
|
|
|
|
|
QPushButton {
|
|
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
|
|
border: 1px solid #c0c0c0;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #333333;
|
|
|
|
|
|
}
|
|
|
|
|
|
QPushButton:hover {
|
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
|
border: 1px solid #a0a0a0;
|
|
|
|
|
|
}
|
|
|
|
|
|
QPushButton:pressed {
|
|
|
|
|
|
background-color: #d0d0d0;
|
|
|
|
|
|
}
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
es_header_layout = QHBoxLayout()
|
|
|
|
|
|
es_header_layout.setContentsMargins(0, 5, 0, 5)
|
|
|
|
|
|
es_header_layout.addWidget(QLabel("<b>🇪🇸 Castilian Spanish Text Element:</b>"))
|
|
|
|
|
|
|
|
|
|
|
|
self.btn_play_sandbox_es = QPushButton("Play 🔊")
|
|
|
|
|
|
self.btn_play_sandbox_es.setFixedWidth(75)
|
|
|
|
|
|
self.btn_play_sandbox_es.setFixedHeight(24)
|
|
|
|
|
|
self.btn_play_sandbox_es.setStyleSheet(button_qss)
|
|
|
|
|
|
self.btn_play_sandbox_es.clicked.connect(self.handle_sandbox_play_es)
|
|
|
|
|
|
es_header_layout.addWidget(self.btn_play_sandbox_es)
|
|
|
|
|
|
|
|
|
|
|
|
self.combo_speed_es = QComboBox()
|
|
|
|
|
|
self.combo_speed_es.addItems(["0.50x", "0.75x", "1.00x", "1.25x", "1.50x"])
|
|
|
|
|
|
self.combo_speed_es.setCurrentText("1.00x")
|
|
|
|
|
|
self.combo_speed_es.setFixedWidth(70)
|
|
|
|
|
|
self.combo_speed_es.setFixedHeight(24)
|
|
|
|
|
|
es_header_layout.addWidget(self.combo_speed_es)
|
|
|
|
|
|
es_header_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
en_header_layout = QHBoxLayout()
|
|
|
|
|
|
en_header_layout.setContentsMargins(0, 5, 0, 5)
|
|
|
|
|
|
en_header_layout.addWidget(QLabel("<b>🇬🇧 English Target Translation:</b>"))
|
|
|
|
|
|
|
|
|
|
|
|
self.btn_play_sandbox_en = QPushButton("Play 🔊")
|
|
|
|
|
|
self.btn_play_sandbox_en.setFixedWidth(75)
|
|
|
|
|
|
self.btn_play_sandbox_en.setFixedHeight(24)
|
|
|
|
|
|
self.btn_play_sandbox_en.setStyleSheet(button_qss)
|
|
|
|
|
|
self.btn_play_sandbox_en.clicked.connect(self.handle_sandbox_play_en)
|
|
|
|
|
|
en_header_layout.addWidget(self.btn_play_sandbox_en)
|
|
|
|
|
|
|
|
|
|
|
|
self.combo_speed_en = QComboBox()
|
|
|
|
|
|
self.combo_speed_en.addItems(["0.50x", "0.75x", "1.00x", "1.25x", "1.50x"])
|
|
|
|
|
|
self.combo_speed_en.setCurrentText("1.00x")
|
|
|
|
|
|
self.combo_speed_en.setFixedWidth(70)
|
|
|
|
|
|
self.combo_speed_en.setFixedHeight(24)
|
|
|
|
|
|
en_header_layout.addWidget(self.combo_speed_en)
|
|
|
|
|
|
en_header_layout.addStretch()
|
2026-06-18 12:23:47 +00:00
|
|
|
|
|
|
|
|
|
|
grammar_header_layout = QHBoxLayout()
|
|
|
|
|
|
grammar_header_layout.setContentsMargins(0, 5, 0, 5)
|
|
|
|
|
|
grammar_header_layout.addWidget(QLabel("<b>📝 Grammar / Usage Note:</b>"))
|
|
|
|
|
|
grammar_header_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
self.input_grammar_note = QTextEdit()
|
|
|
|
|
|
self.input_grammar_note.setMaximumHeight(75)
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.input_grammar_note.setPlaceholderText("e.g., feminine variant...")
|
2026-06-16 05:06:08 +00:00
|
|
|
|
|
|
|
|
|
|
form_layout.addRow("<b>Translation Link ID:</b>", self.input_tx_id)
|
|
|
|
|
|
form_layout.addRow(es_header_layout)
|
|
|
|
|
|
form_layout.addRow(self.input_text_es)
|
|
|
|
|
|
form_layout.addRow(en_header_layout)
|
|
|
|
|
|
form_layout.addRow(self.input_text_en)
|
2026-06-18 12:23:47 +00:00
|
|
|
|
form_layout.addRow(grammar_header_layout)
|
|
|
|
|
|
form_layout.addRow(self.input_grammar_note)
|
2026-06-16 05:06:08 +00:00
|
|
|
|
form_layout.addRow("Classification Profile:", self.combo_type)
|
|
|
|
|
|
form_layout.addRow("Source Context ID (Raw):", self.input_context)
|
2026-06-18 12:47:29 +00:00
|
|
|
|
form_layout.addRow("Anki Note Tags:", self.input_tags)
|
2026-06-16 05:06:08 +00:00
|
|
|
|
form_layout.addRow("<b>Target Deck Scope:</b>", self.input_deck_tag)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
crud_buttons = QHBoxLayout()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.btn_save = QPushButton("➕ Create Pair")
|
|
|
|
|
|
self.btn_update = QPushButton("💾 Update Node")
|
|
|
|
|
|
self.btn_delete = QPushButton("🗑️ Sever Link")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.btn_save.clicked.connect(self.crud_create_pair)
|
|
|
|
|
|
self.btn_update.clicked.connect(self.crud_update_pair)
|
|
|
|
|
|
self.btn_delete.clicked.connect(self.crud_delete_pair)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
crud_buttons.addWidget(self.btn_save)
|
|
|
|
|
|
crud_buttons.addWidget(self.btn_update)
|
|
|
|
|
|
crud_buttons.addWidget(self.btn_delete)
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
right_panel.addWidget(QLabel("<h3>Translation Node Management Matrix</h3>"))
|
2026-06-14 09:56:43 +00:00
|
|
|
|
right_panel.addWidget(form_frame)
|
|
|
|
|
|
right_panel.addLayout(crud_buttons)
|
|
|
|
|
|
right_panel.addStretch()
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
layout.addLayout(left_panel, stretch=4)
|
|
|
|
|
|
layout.addLayout(right_panel, stretch=3)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
self.tabs.addTab(tab, "🗄️ Phrase Sandbox (CRUD)")
|
|
|
|
|
|
|
|
|
|
|
|
# =====================================================================
|
2026-06-19 05:09:05 +00:00
|
|
|
|
# 🃏 TAB 2: FLASHCARD STUDY MODULE
|
2026-06-14 09:56:43 +00:00
|
|
|
|
# =====================================================================
|
|
|
|
|
|
def init_flashcard_reviewer_tab(self):
|
|
|
|
|
|
tab = QWidget()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
layout = QHBoxLayout(tab)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-19 03:42:28 +00:00
|
|
|
|
left_panel = QVBoxLayout()
|
|
|
|
|
|
|
|
|
|
|
|
filter_layout = QHBoxLayout()
|
|
|
|
|
|
filter_layout.addWidget(QLabel("📂 Context:"))
|
|
|
|
|
|
self.review_context_filter = QLineEdit()
|
|
|
|
|
|
self.review_context_filter.setPlaceholderText("Filter Context...")
|
|
|
|
|
|
self.review_context_filter.textChanged.connect(self.refresh_review_table)
|
|
|
|
|
|
filter_layout.addWidget(self.review_context_filter)
|
|
|
|
|
|
|
|
|
|
|
|
filter_layout.addWidget(QLabel("🏷️ Tag:"))
|
|
|
|
|
|
self.review_tag_filter = QLineEdit()
|
|
|
|
|
|
self.review_tag_filter.setPlaceholderText("Filter Tag...")
|
|
|
|
|
|
self.review_tag_filter.textChanged.connect(self.refresh_review_table)
|
|
|
|
|
|
filter_layout.addWidget(self.review_tag_filter)
|
|
|
|
|
|
|
|
|
|
|
|
left_panel.addLayout(filter_layout)
|
|
|
|
|
|
|
|
|
|
|
|
self.review_table = QTableWidget()
|
|
|
|
|
|
self.review_table.setColumnCount(4)
|
|
|
|
|
|
self.review_table.setHorizontalHeaderLabels(["Tx ID", "Spanish Phrase", "Context", "Tags"])
|
|
|
|
|
|
self.review_table.itemSelectionChanged.connect(self.handle_review_table_select)
|
|
|
|
|
|
left_panel.addWidget(self.review_table)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addLayout(left_panel, stretch=4)
|
|
|
|
|
|
|
|
|
|
|
|
right_panel = QVBoxLayout()
|
|
|
|
|
|
|
2026-06-14 09:56:43 +00:00
|
|
|
|
card_frame = QFrame()
|
|
|
|
|
|
card_frame.setStyleSheet("background-color: #ffffff; border: 2px solid #bdc3c7; border-radius: 12px;")
|
|
|
|
|
|
card_layout = QVBoxLayout(card_frame)
|
2026-06-19 03:42:28 +00:00
|
|
|
|
card_frame.setMinimumHeight(280)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.lbl_card_text = QLabel("Select a row or click 'Next Card' to initiate...")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.lbl_card_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.lbl_card_text.setFont(QFont("Arial", 20, QFont.Weight.Bold))
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.lbl_card_text.setWordWrap(True)
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.lbl_card_text.setStyleSheet("color: #2c3e50; border: none; padding: 20px;")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
self.lbl_card_meta = QLabel("")
|
|
|
|
|
|
self.lbl_card_meta.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
|
|
self.lbl_card_meta.setFont(QFont("Arial", 11))
|
|
|
|
|
|
self.lbl_card_meta.setStyleSheet("color: #7f8c8d; border: none;")
|
|
|
|
|
|
|
|
|
|
|
|
card_layout.addStretch()
|
|
|
|
|
|
card_layout.addWidget(self.lbl_card_text)
|
|
|
|
|
|
card_layout.addWidget(self.lbl_card_meta)
|
|
|
|
|
|
card_layout.addStretch()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
right_panel.addWidget(card_frame, stretch=4)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
playback_layout = QHBoxLayout()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
playback_layout.addWidget(QLabel("🔊 Voice Speed:"))
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.slider_review_speed = QSlider(Qt.Orientation.Horizontal)
|
|
|
|
|
|
self.slider_review_speed.setMinimum(50)
|
|
|
|
|
|
self.slider_review_speed.setMaximum(150)
|
|
|
|
|
|
self.slider_review_speed.setValue(100)
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.lbl_review_speed = QLabel("1.00x")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.slider_review_speed.valueChanged.connect(self.handle_live_speed_change)
|
|
|
|
|
|
playback_layout.addWidget(self.slider_review_speed)
|
|
|
|
|
|
playback_layout.addWidget(self.lbl_review_speed)
|
2026-06-19 03:42:28 +00:00
|
|
|
|
right_panel.addLayout(playback_layout)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
action_buttons = QHBoxLayout()
|
|
|
|
|
|
self.btn_play_voice = QPushButton("🗣️ Play Voice Track")
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.btn_flip_card = QPushButton("👁️ Reveal English Partner")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
self.btn_play_voice.clicked.connect(self.handle_play_voice)
|
|
|
|
|
|
self.btn_flip_card.clicked.connect(self.handle_flip_card)
|
|
|
|
|
|
|
|
|
|
|
|
action_buttons.addWidget(self.btn_play_voice)
|
|
|
|
|
|
action_buttons.addWidget(self.btn_flip_card)
|
2026-06-19 03:42:28 +00:00
|
|
|
|
right_panel.addLayout(action_buttons)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-19 03:42:28 +00:00
|
|
|
|
right_panel.addSpacing(15)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-19 03:42:28 +00:00
|
|
|
|
bottom_utility_layout = QHBoxLayout()
|
|
|
|
|
|
self.btn_export_anki = QPushButton("📦 Export Anki Deck")
|
|
|
|
|
|
self.btn_export_video = QPushButton("🎬 Export Video")
|
|
|
|
|
|
self.btn_load_next = QPushButton("➡️ Next Card")
|
|
|
|
|
|
|
|
|
|
|
|
self.btn_export_anki.clicked.connect(self.handle_export_anki_deck)
|
|
|
|
|
|
self.btn_export_video.clicked.connect(self.handle_export_video_assets)
|
|
|
|
|
|
self.btn_load_next.clicked.connect(self.handle_load_next_card)
|
|
|
|
|
|
|
|
|
|
|
|
utility_qss = "QPushButton { font-weight: bold; background-color: #eaf2f8; padding: 6px; border-radius: 4px; }"
|
|
|
|
|
|
self.btn_export_anki.setStyleSheet(utility_qss)
|
|
|
|
|
|
self.btn_export_video.setStyleSheet(utility_qss)
|
|
|
|
|
|
self.btn_load_next.setStyleSheet("QPushButton { font-weight: bold; background-color: #d5f5e3; padding: 6px; border-radius: 4px; }")
|
|
|
|
|
|
|
|
|
|
|
|
bottom_utility_layout.addWidget(self.btn_export_anki)
|
|
|
|
|
|
bottom_utility_layout.addWidget(self.btn_export_video)
|
|
|
|
|
|
bottom_utility_layout.addStretch()
|
|
|
|
|
|
bottom_utility_layout.addWidget(self.btn_load_next)
|
|
|
|
|
|
right_panel.addLayout(bottom_utility_layout)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addLayout(right_panel, stretch=3)
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.tabs.addTab(tab, "🃏 Flashcard Review")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-19 05:09:05 +00:00
|
|
|
|
# =====================================================================
|
|
|
|
|
|
# ⚙️ TAB 3: SYSTEM HARDWARE & EXPORT SETTINGS
|
|
|
|
|
|
# =====================================================================
|
|
|
|
|
|
def init_settings_tab(self):
|
|
|
|
|
|
tab = QWidget()
|
|
|
|
|
|
layout = QVBoxLayout(tab)
|
|
|
|
|
|
|
|
|
|
|
|
settings_frame = QFrame()
|
|
|
|
|
|
settings_frame.setFrameShape(QFrame.Shape.StyledPanel)
|
|
|
|
|
|
form_layout = QFormLayout(settings_frame)
|
|
|
|
|
|
|
|
|
|
|
|
anki_layout = QHBoxLayout()
|
|
|
|
|
|
self.line_anki_dir = QLineEdit(self.anki_export_dir)
|
|
|
|
|
|
self.line_anki_dir.setReadOnly(True)
|
|
|
|
|
|
btn_browse_anki = QPushButton("Browse 📂")
|
|
|
|
|
|
btn_browse_anki.clicked.connect(self.handle_browse_anki_directory)
|
|
|
|
|
|
anki_layout.addWidget(self.line_anki_dir)
|
|
|
|
|
|
anki_layout.addWidget(btn_browse_anki)
|
|
|
|
|
|
|
|
|
|
|
|
video_layout = QHBoxLayout()
|
|
|
|
|
|
self.line_video_dir = QLineEdit(self.video_export_dir)
|
|
|
|
|
|
self.line_video_dir.setReadOnly(True)
|
|
|
|
|
|
btn_browse_video = QPushButton("Browse 📂")
|
|
|
|
|
|
btn_browse_video.clicked.connect(self.handle_browse_video_directory)
|
|
|
|
|
|
video_layout.addWidget(self.line_video_dir)
|
|
|
|
|
|
video_layout.addWidget(btn_browse_video)
|
|
|
|
|
|
|
|
|
|
|
|
form_layout.addRow("<b>Anki Deck Export Destination:</b>", anki_layout)
|
|
|
|
|
|
form_layout.addRow("<b>Video Assembly Output Target:</b>", video_layout)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(QLabel("<h2>Application Preferences & Workspace Routing</h2>"))
|
|
|
|
|
|
layout.addWidget(settings_frame)
|
|
|
|
|
|
layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
self.tabs.addTab(tab, "⚙️ Settings")
|
|
|
|
|
|
|
|
|
|
|
|
def handle_browse_anki_directory(self):
|
|
|
|
|
|
directory = QFileDialog.getExistingDirectory(self, "Select Anki Export Folder", self.anki_export_dir)
|
|
|
|
|
|
if directory:
|
|
|
|
|
|
self.anki_export_dir = directory
|
|
|
|
|
|
self.line_anki_dir.setText(directory)
|
|
|
|
|
|
self.save_setting_to_db("anki_export_directory", directory)
|
|
|
|
|
|
|
|
|
|
|
|
def handle_browse_video_directory(self):
|
|
|
|
|
|
directory = QFileDialog.getExistingDirectory(self, "Select Video Export Folder", self.video_export_dir)
|
|
|
|
|
|
if directory:
|
|
|
|
|
|
self.video_export_dir = directory
|
|
|
|
|
|
self.line_video_dir.setText(directory)
|
|
|
|
|
|
self.save_setting_to_db("video_export_directory", directory)
|
|
|
|
|
|
|
2026-06-14 09:56:43 +00:00
|
|
|
|
# =====================================================================
|
2026-06-19 03:42:28 +00:00
|
|
|
|
# ⚡ DATA MATRIX CONTROL & SYNCHRONIZATION VIEWS
|
2026-06-14 09:56:43 +00:00
|
|
|
|
# =====================================================================
|
|
|
|
|
|
def refresh_crud_table(self):
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
text_filter = self.search_text_input.text().strip()
|
|
|
|
|
|
context_filter = self.search_context_input.text().strip()
|
|
|
|
|
|
|
|
|
|
|
|
query = """
|
2026-06-19 03:42:28 +00:00
|
|
|
|
SELECT t.translation_id, p1.text, p2.text, p1.word_type, p1.source_context, t.deck_name
|
2026-06-16 05:06:08 +00:00
|
|
|
|
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'
|
|
|
|
|
|
"""
|
|
|
|
|
|
params = []
|
|
|
|
|
|
|
|
|
|
|
|
if text_filter:
|
|
|
|
|
|
query += " AND (p1.text LIKE ? OR p2.text LIKE ?)"
|
|
|
|
|
|
params.extend([f"%{text_filter}%", f"%{text_filter}%"])
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
if context_filter:
|
|
|
|
|
|
query += " AND p1.source_context LIKE ?"
|
|
|
|
|
|
params.append(f"%{context_filter}%")
|
|
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY t.translation_id ASC LIMIT 250"
|
|
|
|
|
|
|
|
|
|
|
|
cursor.execute(query, params)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
rows = cursor.fetchall()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.translation_table.setRowCount(0)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
for row_idx, row_data in enumerate(rows):
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.translation_table.insertRow(row_idx)
|
|
|
|
|
|
for col_idx in range(6):
|
|
|
|
|
|
val = row_data[col_idx]
|
|
|
|
|
|
self.translation_table.setItem(row_idx, col_idx, QTableWidgetItem(str(val if val is not None else "")))
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-19 03:42:28 +00:00
|
|
|
|
def refresh_review_table(self):
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
context_filter = self.review_context_filter.text().strip()
|
|
|
|
|
|
tag_filter = self.review_tag_filter.text().strip()
|
|
|
|
|
|
|
|
|
|
|
|
query = """
|
|
|
|
|
|
SELECT t.translation_id, p1.text, p1.source_context, t.tags, p1.id
|
|
|
|
|
|
FROM translations t
|
|
|
|
|
|
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
|
|
|
|
|
WHERE p1.language = 'es'
|
|
|
|
|
|
"""
|
|
|
|
|
|
params = []
|
|
|
|
|
|
if context_filter:
|
|
|
|
|
|
query += " AND p1.source_context LIKE ?"
|
|
|
|
|
|
params.append(f"%{context_filter}%")
|
|
|
|
|
|
if tag_filter:
|
|
|
|
|
|
query += " AND t.tags LIKE ?"
|
|
|
|
|
|
params.append(f"%{tag_filter}%")
|
|
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY t.translation_id ASC"
|
|
|
|
|
|
|
|
|
|
|
|
cursor.execute(query, params)
|
|
|
|
|
|
rows = cursor.fetchall()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
self.review_table.setRowCount(0)
|
|
|
|
|
|
self.flashcard_ids_pool = []
|
|
|
|
|
|
|
|
|
|
|
|
for row_idx, row_data in enumerate(rows):
|
|
|
|
|
|
self.review_table.insertRow(row_idx)
|
2026-06-19 05:09:05 +00:00
|
|
|
|
self.flashcard_ids_pool.append(row_data[4])
|
2026-06-19 03:42:28 +00:00
|
|
|
|
for col_idx in range(4):
|
|
|
|
|
|
val = row_data[col_idx]
|
|
|
|
|
|
self.review_table.setItem(row_idx, col_idx, QTableWidgetItem(str(val if val is not None else "")))
|
|
|
|
|
|
|
2026-06-14 09:56:43 +00:00
|
|
|
|
def handle_table_row_select(self):
|
2026-06-16 05:06:08 +00:00
|
|
|
|
selected_ranges = self.translation_table.selectedRanges()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
if not selected_ranges:
|
|
|
|
|
|
return
|
|
|
|
|
|
row = selected_ranges[0].topRow()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
tx_id_item = self.translation_table.item(row, 0)
|
|
|
|
|
|
if not tx_id_item:
|
2026-06-14 09:56:43 +00:00
|
|
|
|
return
|
2026-06-16 05:06:08 +00:00
|
|
|
|
|
|
|
|
|
|
tx_id = tx_id_item.text()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
cursor.execute("""
|
2026-06-18 12:47:29 +00:00
|
|
|
|
SELECT t.translation_id, p1.text, p2.text, p1.word_type, p1.source_context, t.deck_name, t.notes, t.tags
|
2026-06-16 05:06:08 +00:00
|
|
|
|
FROM translations t
|
|
|
|
|
|
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
|
|
|
|
|
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
|
|
|
|
|
WHERE t.translation_id = ?
|
|
|
|
|
|
""", (tx_id,))
|
2026-06-14 09:56:43 +00:00
|
|
|
|
record = cursor.fetchone()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
if record:
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.input_tx_id.setText(str(record[0]))
|
|
|
|
|
|
self.input_text_es.setPlainText(str(record[1]))
|
|
|
|
|
|
self.input_text_en.setPlainText(str(record[2]))
|
|
|
|
|
|
self.combo_type.setCurrentText(str(record[3]) if record[3] else "phrase")
|
|
|
|
|
|
self.input_context.setText(str(record[4]) if record[4] else "")
|
2026-06-18 12:47:29 +00:00
|
|
|
|
self.input_tags.setText(str(record[7]) if record[7] is not None else "")
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.input_deck_tag.setText(str(record[5]) if record[5] else "General")
|
2026-06-18 12:23:47 +00:00
|
|
|
|
self.input_grammar_note.setPlainText(str(record[6]) if record[6] is not None else "")
|
2026-06-16 05:06:08 +00:00
|
|
|
|
|
2026-06-19 03:42:28 +00:00
|
|
|
|
def handle_review_table_select(self):
|
|
|
|
|
|
selected_ranges = self.review_table.selectedRanges()
|
|
|
|
|
|
if not selected_ranges:
|
|
|
|
|
|
return
|
|
|
|
|
|
row = selected_ranges[0].topRow()
|
|
|
|
|
|
phrase_id = self.flashcard_ids_pool[row]
|
|
|
|
|
|
self.load_flashcard_by_id(phrase_id)
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
def step_table_row(self, direction):
|
|
|
|
|
|
current_row = self.translation_table.currentRow()
|
|
|
|
|
|
next_row = current_row + direction
|
|
|
|
|
|
if 0 <= next_row < self.translation_table.rowCount():
|
|
|
|
|
|
self.translation_table.setCurrentCell(next_row, 0)
|
|
|
|
|
|
|
2026-06-19 03:42:28 +00:00
|
|
|
|
# =====================================================================
|
|
|
|
|
|
# ➕ ENGINE ATOMIC OPERATIONS LOGIC (CRUD MODIFIERS)
|
|
|
|
|
|
# =====================================================================
|
2026-06-16 05:06:08 +00:00
|
|
|
|
def crud_create_pair(self):
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("""
|
|
|
|
|
|
INSERT INTO phrases (text, language, word_type, source_context) VALUES (?, 'es', ?, ?)
|
|
|
|
|
|
""", (self.input_text_es.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip()))
|
|
|
|
|
|
es_id = cursor.lastrowid
|
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("""
|
|
|
|
|
|
INSERT INTO phrases (text, language, word_type, source_context) VALUES (?, 'en', ?, ?)
|
|
|
|
|
|
""", (self.input_text_en.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip()))
|
|
|
|
|
|
en_id = cursor.lastrowid
|
|
|
|
|
|
|
|
|
|
|
|
cursor.execute("""
|
2026-06-18 12:47:29 +00:00
|
|
|
|
INSERT INTO translations (source_phrase_id, target_phrase_id, deck_name, notes, tags) VALUES (?, ?, ?, ?, ?)
|
|
|
|
|
|
""", (es_id, en_id, self.input_deck_tag.text().strip() or "General", self.input_grammar_note.toPlainText().strip(), self.input_tags.text().strip()))
|
2026-06-16 05:06:08 +00:00
|
|
|
|
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
self.refresh_crud_table()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.refresh_review_table()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
QMessageBox.information(self, "Success", "Isolated phrase pairs created and relational link bound.")
|
|
|
|
|
|
|
|
|
|
|
|
def crud_update_pair(self):
|
|
|
|
|
|
tx_id = self.input_tx_id.text()
|
|
|
|
|
|
if not tx_id:
|
|
|
|
|
|
return
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
cursor.execute("SELECT source_phrase_id, target_phrase_id FROM translations WHERE translation_id = ?", (tx_id,))
|
|
|
|
|
|
ids = cursor.fetchone()
|
|
|
|
|
|
|
|
|
|
|
|
if ids:
|
|
|
|
|
|
es_id, en_id = ids
|
|
|
|
|
|
cursor.execute("UPDATE phrases SET text=?, word_type=?, source_context=? WHERE id=?",
|
|
|
|
|
|
(self.input_text_es.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip(), es_id))
|
|
|
|
|
|
cursor.execute("UPDATE phrases SET text=?, word_type=?, source_context=? WHERE id=?",
|
|
|
|
|
|
(self.input_text_en.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip(), en_id))
|
2026-06-18 12:47:29 +00:00
|
|
|
|
cursor.execute("UPDATE translations SET deck_name=?, notes=?, tags=? WHERE translation_id=?",
|
|
|
|
|
|
(self.input_deck_tag.text().strip() or "General", self.input_grammar_note.toPlainText().strip(), self.input_tags.text().strip(), tx_id))
|
2026-06-16 05:06:08 +00:00
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
2026-06-14 09:56:43 +00:00
|
|
|
|
conn.close()
|
|
|
|
|
|
self.refresh_crud_table()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.refresh_review_table()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
QMessageBox.information(self, "Success", "Relational node structural update complete.")
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
def crud_delete_pair(self):
|
|
|
|
|
|
tx_id = self.input_tx_id.text()
|
|
|
|
|
|
if not tx_id:
|
2026-06-14 09:56:43 +00:00
|
|
|
|
return
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
cursor.execute("SELECT source_phrase_id, target_phrase_id FROM translations WHERE translation_id = ?", (tx_id,))
|
|
|
|
|
|
ids = cursor.fetchone()
|
|
|
|
|
|
if ids:
|
|
|
|
|
|
es_id, en_id = ids
|
|
|
|
|
|
cursor.execute("DELETE FROM translations WHERE translation_id=?", (tx_id,))
|
|
|
|
|
|
cursor.execute("DELETE FROM phrases WHERE id=?", (es_id,))
|
|
|
|
|
|
cursor.execute("DELETE FROM phrases WHERE id=?", (en_id,))
|
|
|
|
|
|
conn.commit()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
conn.close()
|
|
|
|
|
|
self.refresh_crud_table()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.refresh_review_table()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
self.input_tx_id.clear()
|
|
|
|
|
|
self.input_text_es.clear()
|
|
|
|
|
|
self.input_text_en.clear()
|
2026-06-18 12:23:47 +00:00
|
|
|
|
self.input_grammar_note.clear()
|
2026-06-18 12:47:29 +00:00
|
|
|
|
self.input_tags.clear()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
# =====================================================================
|
2026-06-19 03:42:28 +00:00
|
|
|
|
# 🔊 AUDIO ENGINE & EXPANDED FLASHCARD ACTIONS
|
2026-06-16 05:06:08 +00:00
|
|
|
|
# =====================================================================
|
2026-06-19 03:42:28 +00:00
|
|
|
|
def load_flashcard_by_id(self, phrase_id):
|
2026-06-14 09:56:43 +00:00
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
2026-06-16 05:06:08 +00:00
|
|
|
|
cursor.execute("""
|
2026-06-19 03:42:28 +00:00
|
|
|
|
SELECT t.translation_id, p1.text, p1.source_context, t.deck_name, p1.id, t.tags
|
2026-06-16 05:06:08 +00:00
|
|
|
|
FROM translations t
|
|
|
|
|
|
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
2026-06-19 03:42:28 +00:00
|
|
|
|
WHERE p1.id = ?
|
|
|
|
|
|
""", (phrase_id,))
|
2026-06-14 09:56:43 +00:00
|
|
|
|
record = cursor.fetchone()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
if record:
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.current_flashcard_id = record[4]
|
2026-06-14 09:56:43 +00:00
|
|
|
|
self.lbl_card_text.setText(record[1])
|
2026-06-19 03:42:28 +00:00
|
|
|
|
self.lbl_card_meta.setText(f"Link ID: {record[0]} • Context: {record[2]} • Tag: {record[5]} • Deck: {record[3]}")
|
|
|
|
|
|
|
|
|
|
|
|
def handle_load_next_card(self):
|
|
|
|
|
|
if not self.flashcard_ids_pool:
|
|
|
|
|
|
QMessageBox.information(self, "Empty Pool", "No flashcards match your selected filter configurations.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
target_id = random.choice(self.flashcard_ids_pool)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
matched_idx = self.flashcard_ids_pool.index(target_id)
|
|
|
|
|
|
self.review_table.setCurrentCell(matched_idx, 0)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
self.load_flashcard_by_id(target_id)
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
def handle_play_voice(self):
|
|
|
|
|
|
if not self.current_flashcard_id:
|
|
|
|
|
|
return
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
cursor.execute("SELECT text, language FROM phrases WHERE id = ?", (self.current_flashcard_id,))
|
2026-06-16 05:06:08 +00:00
|
|
|
|
row = cursor.fetchone()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
conn.close()
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
if row:
|
2026-06-19 03:42:28 +00:00
|
|
|
|
text_str, lang = row
|
2026-06-18 12:23:47 +00:00
|
|
|
|
safe_name = "".join([c for c in text_str if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
2026-06-19 03:42:28 +00:00
|
|
|
|
os.makedirs("media", exist_ok=True)
|
|
|
|
|
|
target_file = f"media/{safe_name}_{lang}_female.mp3"
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
2026-06-19 03:42:28 +00:00
|
|
|
|
if not os.path.exists(target_file):
|
|
|
|
|
|
print(f"🔊 Review Fallback: Synthesizing missing audio asset on the fly for '{text_str}'...")
|
|
|
|
|
|
try:
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import edge_tts
|
|
|
|
|
|
communicate = edge_tts.Communicate(text_str, "es-ES-ElviraNeural")
|
|
|
|
|
|
asyncio.run(communicate.save(target_file))
|
|
|
|
|
|
except Exception as tts_err:
|
|
|
|
|
|
QMessageBox.critical(self, "TTS Error", f"Review pipeline failed to synthesize track:\n{tts_err}")
|
|
|
|
|
|
return
|
2026-06-16 05:06:08 +00:00
|
|
|
|
|
|
|
|
|
|
if os.path.exists(target_file):
|
|
|
|
|
|
self.media_player.setSource(QUrl.fromLocalFile(os.path.abspath(target_file)))
|
|
|
|
|
|
self.media_player.setPlaybackRate(self.slider_review_speed.value() / 100.0)
|
|
|
|
|
|
self.media_player.play()
|
2026-06-14 09:56:43 +00:00
|
|
|
|
|
|
|
|
|
|
def handle_flip_card(self):
|
|
|
|
|
|
if not self.current_flashcard_id:
|
|
|
|
|
|
return
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
cursor.execute("""
|
2026-06-18 12:23:47 +00:00
|
|
|
|
SELECT p2.text, t.notes FROM translations t
|
2026-06-16 05:06:08 +00:00
|
|
|
|
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
2026-06-14 09:56:43 +00:00
|
|
|
|
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
2026-06-16 05:06:08 +00:00
|
|
|
|
WHERE p1.id = ?
|
2026-06-14 09:56:43 +00:00
|
|
|
|
""", (self.current_flashcard_id,))
|
|
|
|
|
|
row = cursor.fetchone()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
if row:
|
2026-06-16 05:06:08 +00:00
|
|
|
|
clean_es = self.lbl_card_text.text().split("\n\n👉")[0]
|
2026-06-18 12:23:47 +00:00
|
|
|
|
display_text = f"{clean_es}\n\n👉 [ {row[0]} ]"
|
|
|
|
|
|
if row[1]:
|
|
|
|
|
|
display_text += f"\n\n💡 Note: {row[1]}"
|
|
|
|
|
|
self.lbl_card_text.setText(display_text)
|
2026-06-12 11:58:47 +00:00
|
|
|
|
|
2026-06-19 03:42:28 +00:00
|
|
|
|
def handle_sandbox_play_es(self):
|
|
|
|
|
|
text_str = self.input_text_es.toPlainText().strip()
|
|
|
|
|
|
if not text_str:
|
|
|
|
|
|
return
|
|
|
|
|
|
safe_name = "".join([c for c in text_str if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
|
|
|
|
|
os.makedirs("media", exist_ok=True)
|
|
|
|
|
|
target_file = f"media/{safe_name}_es_female.mp3"
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(target_file):
|
|
|
|
|
|
try:
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import edge_tts
|
|
|
|
|
|
communicate = edge_tts.Communicate(text_str, "es-ES-ElviraNeural")
|
|
|
|
|
|
asyncio.run(communicate.save(target_file))
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
QMessageBox.critical(self, "TTS Error", str(e))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.media_player.setSource(QUrl.fromLocalFile(os.path.abspath(target_file)))
|
|
|
|
|
|
self.media_player.setPlaybackRate(float(self.combo_speed_es.currentText().replace("x", "")))
|
|
|
|
|
|
self.media_player.play()
|
|
|
|
|
|
|
|
|
|
|
|
def handle_sandbox_play_en(self):
|
|
|
|
|
|
text_str = self.input_text_en.toPlainText().strip()
|
|
|
|
|
|
if not text_str:
|
|
|
|
|
|
return
|
|
|
|
|
|
safe_name = "".join([c for c in text_str if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
|
|
|
|
|
os.makedirs("media", exist_ok=True)
|
|
|
|
|
|
target_file = f"media/{safe_name}_en_female.mp3"
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(target_file):
|
|
|
|
|
|
try:
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import edge_tts
|
|
|
|
|
|
communicate = edge_tts.Communicate(text_str, "en-GB-SoniaNeural")
|
|
|
|
|
|
asyncio.run(communicate.save(target_file))
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
QMessageBox.critical(self, "TTS Error", str(e))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.media_player.setSource(QUrl.fromLocalFile(os.path.abspath(target_file)))
|
|
|
|
|
|
self.media_player.setPlaybackRate(float(self.combo_speed_en.currentText().replace("x", "")))
|
|
|
|
|
|
self.media_player.play()
|
|
|
|
|
|
|
|
|
|
|
|
def handle_live_speed_change(self, value):
|
|
|
|
|
|
rate = value / 100.0
|
|
|
|
|
|
self.lbl_review_speed.setText(f"{rate:.2f}x")
|
|
|
|
|
|
if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
|
|
|
|
|
|
self.media_player.setPlaybackRate(rate)
|
|
|
|
|
|
|
|
|
|
|
|
# =====================================================================
|
2026-06-19 05:48:15 +00:00
|
|
|
|
# 📦 ARTIFACT EXPORT GATEWAYS (GENANKI LIVE ENGINE WITH DUAL AUDIO)
|
2026-06-19 03:42:28 +00:00
|
|
|
|
# =====================================================================
|
|
|
|
|
|
def handle_export_anki_deck(self):
|
2026-06-19 05:48:15 +00:00
|
|
|
|
"""Compiles active subset into functional .apkg with bundled Spanish and English audio tracks."""
|
2026-06-19 05:30:59 +00:00
|
|
|
|
if not self.flashcard_ids_pool:
|
|
|
|
|
|
QMessageBox.warning(self, "Export Cancelled", "The current study stack is empty. Verify your search filters.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 1. Generate Stable Cryptographic Note Model ID
|
2026-06-19 05:48:15 +00:00
|
|
|
|
model_hash = hashlib.sha256(b"castilian_voice_trainer_model_v2").hexdigest()
|
2026-06-19 05:30:59 +00:00
|
|
|
|
model_id = int(model_hash[:13], 16)
|
|
|
|
|
|
|
2026-06-19 05:48:15 +00:00
|
|
|
|
# Standardized Castilian Note Template Structure Definition (Includes English Audio)
|
2026-06-19 05:30:59 +00:00
|
|
|
|
spanish_note_model = genanki.Model(
|
|
|
|
|
|
model_id,
|
2026-06-19 05:48:15 +00:00
|
|
|
|
'Castilian Audio Flashcard Model v2',
|
2026-06-19 05:30:59 +00:00
|
|
|
|
fields=[
|
|
|
|
|
|
{'name': 'SpanishPhrase'},
|
|
|
|
|
|
{'name': 'EnglishTranslation'},
|
|
|
|
|
|
{'name': 'GrammarNotes'},
|
2026-06-19 05:48:15 +00:00
|
|
|
|
{'name': 'SpanishAudio'},
|
|
|
|
|
|
{'name': 'EnglishAudio'}
|
2026-06-19 05:30:59 +00:00
|
|
|
|
],
|
|
|
|
|
|
templates=[
|
|
|
|
|
|
{
|
|
|
|
|
|
'name': 'Card 1: Auditory Identification',
|
2026-06-19 05:48:15 +00:00
|
|
|
|
'qfmt': (
|
|
|
|
|
|
'<div style="font-family: Arial; font-size: 24px; text-align: center; color: #2c3e50;">{{SpanishPhrase}}</div>'
|
|
|
|
|
|
'<br><div style="text-align: center;">{{SpanishAudio}}</div>'
|
|
|
|
|
|
),
|
|
|
|
|
|
'afmt': (
|
|
|
|
|
|
'{{FrontSide}}<hr id="answer">'
|
|
|
|
|
|
'<div style="font-family: Arial; font-size: 20px; text-align: center; color: #27ae60; font-weight: bold;">{{EnglishTranslation}}</div>'
|
|
|
|
|
|
'<div style="text-align: center; margin-top: 5px;">{{EnglishAudio}}</div><br>'
|
|
|
|
|
|
'<div style="font-family: Arial; font-size: 14px; text-align: center; color: #7f8c8d; font-style: italic;">{{GrammarNotes}}</div>'
|
|
|
|
|
|
),
|
2026-06-19 05:30:59 +00:00
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
css='.card { font-family: arial; font-size: 20px; text-align: center; background-color: #f8f9fa; }'
|
2026-06-19 03:42:28 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-19 05:30:59 +00:00
|
|
|
|
# Determine dynamic manifest names based on runtime filter choices
|
|
|
|
|
|
context_txt = self.review_context_filter.text().strip()
|
|
|
|
|
|
tag_txt = self.review_tag_filter.text().strip()
|
|
|
|
|
|
|
|
|
|
|
|
if context_txt and tag_txt:
|
|
|
|
|
|
file_title = f"Spanish_Export_Context_{context_txt}_Tag_{tag_txt}.apkg"
|
|
|
|
|
|
elif context_txt:
|
|
|
|
|
|
file_title = f"Spanish_Export_Context_{context_txt}.apkg"
|
|
|
|
|
|
elif tag_txt:
|
|
|
|
|
|
file_title = f"Spanish_Export_Tag_{tag_txt}.apkg"
|
|
|
|
|
|
else:
|
|
|
|
|
|
file_title = "Spanish_Master_Deck.apkg"
|
|
|
|
|
|
|
|
|
|
|
|
# Clean filename characters for safety across platforms
|
|
|
|
|
|
file_title = "".join([c for c in file_title if c.isalnum() or c in (".", "_", "-")]).strip()
|
|
|
|
|
|
destination_path = os.path.join(self.anki_export_dir, file_title)
|
|
|
|
|
|
|
|
|
|
|
|
decks_map = {}
|
|
|
|
|
|
media_files_manifest = []
|
2026-06-19 05:48:15 +00:00
|
|
|
|
missing_es_audio = 0
|
|
|
|
|
|
missing_en_audio = 0
|
2026-06-19 05:30:59 +00:00
|
|
|
|
|
|
|
|
|
|
# 2. Fetch specific database rows matching the current runtime array pool
|
|
|
|
|
|
conn = get_connection()
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
placeholders = ",".join(["?"] * len(self.flashcard_ids_pool))
|
|
|
|
|
|
query = f"""
|
2026-06-19 05:48:15 +00:00
|
|
|
|
SELECT t.deck_name, p1.text, p2.text, t.notes, t.tags
|
2026-06-19 05:30:59 +00:00
|
|
|
|
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.id IN ({placeholders})
|
|
|
|
|
|
"""
|
|
|
|
|
|
cursor.execute(query, self.flashcard_ids_pool)
|
|
|
|
|
|
records = cursor.fetchall()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
# 3. Iterate over records and construct notes
|
|
|
|
|
|
for row in records:
|
|
|
|
|
|
db_deck_name = row[0].strip() if row[0] else "Castilian Spanish Master"
|
|
|
|
|
|
es_text = row[1].strip()
|
|
|
|
|
|
en_text = row[2].strip()
|
|
|
|
|
|
notes_text = row[3].strip() if row[3] else ""
|
|
|
|
|
|
tags_string = row[4].strip() if row[4] else ""
|
|
|
|
|
|
|
2026-06-19 05:48:15 +00:00
|
|
|
|
# --- Handle Spanish Audio Mapping ---
|
|
|
|
|
|
safe_es_audio_name = "".join([c for c in es_text if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
|
|
|
|
|
relative_es_path = f"media/{safe_es_audio_name}_es_female.mp3"
|
|
|
|
|
|
es_filename_only = f"{safe_es_audio_name}_es_female.mp3"
|
2026-06-19 05:30:59 +00:00
|
|
|
|
|
2026-06-19 05:48:15 +00:00
|
|
|
|
if os.path.exists(relative_es_path):
|
|
|
|
|
|
if relative_es_path not in media_files_manifest:
|
|
|
|
|
|
media_files_manifest.append(relative_es_path)
|
|
|
|
|
|
anki_es_audio_field = f"[sound:{es_filename_only}]"
|
2026-06-19 05:30:59 +00:00
|
|
|
|
else:
|
2026-06-19 05:48:15 +00:00
|
|
|
|
missing_es_audio += 1
|
|
|
|
|
|
anki_es_audio_field = ""
|
|
|
|
|
|
|
|
|
|
|
|
# --- Handle English Audio Mapping ---
|
|
|
|
|
|
safe_en_audio_name = "".join([c for c in en_text if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
|
|
|
|
|
relative_en_path = f"media/{safe_en_audio_name}_en_female.mp3"
|
|
|
|
|
|
en_filename_only = f"{safe_en_audio_name}_en_female.mp3"
|
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists(relative_en_path):
|
|
|
|
|
|
if relative_en_path not in media_files_manifest:
|
|
|
|
|
|
media_files_manifest.append(relative_en_path)
|
|
|
|
|
|
anki_en_audio_field = f"[sound:{en_filename_only}]"
|
|
|
|
|
|
else:
|
|
|
|
|
|
missing_en_audio += 1
|
|
|
|
|
|
anki_en_audio_field = ""
|
2026-06-19 05:30:59 +00:00
|
|
|
|
|
|
|
|
|
|
# Handle dynamic deck assignment structure
|
|
|
|
|
|
if db_deck_name not in decks_map:
|
|
|
|
|
|
deck_hash = hashlib.sha256(db_deck_name.encode('utf-8')).hexdigest()
|
|
|
|
|
|
deck_id = int(deck_hash[:13], 16)
|
|
|
|
|
|
decks_map[db_deck_name] = genanki.Deck(deck_id, db_deck_name)
|
|
|
|
|
|
|
|
|
|
|
|
# Split tags string by spaces into an array list for genanki
|
|
|
|
|
|
parsed_tags = [t for t in tags_string.replace(",", " ").split(" ") if t]
|
|
|
|
|
|
|
2026-06-19 05:48:15 +00:00
|
|
|
|
# Construct unique note entry template matching updated fields array
|
2026-06-19 05:30:59 +00:00
|
|
|
|
flash_note = genanki.Note(
|
|
|
|
|
|
model=spanish_note_model,
|
2026-06-19 05:48:15 +00:00
|
|
|
|
fields=[es_text, en_text, notes_text, anki_es_audio_field, anki_en_audio_field],
|
2026-06-19 05:30:59 +00:00
|
|
|
|
tags=parsed_tags
|
|
|
|
|
|
)
|
|
|
|
|
|
decks_map[db_deck_name].add_note(flash_note)
|
|
|
|
|
|
|
|
|
|
|
|
# 4. Package all deck components into an .apkg container
|
|
|
|
|
|
try:
|
|
|
|
|
|
package = genanki.Package(list(decks_map.values()))
|
|
|
|
|
|
package.media_files = media_files_manifest
|
|
|
|
|
|
package.write_to_file(destination_path)
|
|
|
|
|
|
|
2026-06-19 05:48:15 +00:00
|
|
|
|
success_msg = (
|
|
|
|
|
|
f"✨ Packaging complete!\n\n"
|
|
|
|
|
|
f"File Output: {file_title}\n"
|
|
|
|
|
|
f"Destination: {self.anki_export_dir}\n"
|
|
|
|
|
|
f"Total Cards Built: {len(records)}\n"
|
|
|
|
|
|
f"Decks Created: {len(decks_map)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if missing_es_audio > 0 or missing_en_audio > 0:
|
|
|
|
|
|
success_msg += f"\n\n⚠️ Note: Missing files detected (Spanish: {missing_es_audio}, English: {missing_en_audio}). " \
|
|
|
|
|
|
f"Cards were bundled without corresponding audio fields if they hadn't been triggered in the review window yet."
|
2026-06-19 05:30:59 +00:00
|
|
|
|
|
|
|
|
|
|
QMessageBox.information(self, "Export Complete", success_msg)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as export_error:
|
|
|
|
|
|
QMessageBox.critical(self, "Export Failed", f"Genanki package compression pipeline failure:\n{export_error}")
|
|
|
|
|
|
|
2026-06-19 03:42:28 +00:00
|
|
|
|
def handle_export_video_assets(self):
|
2026-06-19 05:09:05 +00:00
|
|
|
|
"""Action handler loop for compiling video cards."""
|
2026-06-19 03:42:28 +00:00
|
|
|
|
QMessageBox.information(
|
|
|
|
|
|
self, "Video Synthesis Suite",
|
|
|
|
|
|
f"Staging visual timeline render frames loop!\n\n"
|
2026-06-19 05:09:05 +00:00
|
|
|
|
f"Target Directory: {self.video_export_dir}\n"
|
|
|
|
|
|
f"Currently Filtered Scope count: {len(self.flashcard_ids_pool)} sequences."
|
2026-06-19 03:42:28 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-16 05:06:08 +00:00
|
|
|
|
# =====================================================================
|
|
|
|
|
|
# 🚀 DIAGNOSTIC STARTUP FRAMEWORK WRAPPER
|
|
|
|
|
|
# =====================================================================
|
2026-06-12 11:58:47 +00:00
|
|
|
|
if __name__ == "__main__":
|
2026-06-16 05:06:08 +00:00
|
|
|
|
print("🚀 Initializing PyQt6 Application Framework...")
|
|
|
|
|
|
try:
|
|
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
|
|
window = SpanishTrainerApp()
|
|
|
|
|
|
window.show()
|
|
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
except Exception as fatal_error:
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
print("\n❌ CRITICAL CRASH DETECTED ON CORE STARTUP THREAD!")
|
|
|
|
|
|
traceback.print_exc()
|
2026-06-18 12:23:47 +00:00
|
|
|
|
sys.exit(1)
|