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 : 1 px solid #c0c0c0;
border - radius : 4 px ;
font - size : 11 px ;
font - weight : bold ;
color : #333333;
}
QPushButton : hover {
background - color : #e0e0e0;
border : 1 px 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:30:59 +00:00
# 📦 ARTIFACT EXPORT GATEWAYS (GENANKI LIVE ENGINE)
2026-06-19 03:42:28 +00:00
# =====================================================================
def handle_export_anki_deck ( self ) :
2026-06-19 05:30:59 +00:00
""" Compiles active subset into functional .apkg with bundled neural audio tracks. """
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
model_hash = hashlib . sha256 ( b " castilian_voice_trainer_model_v1 " ) . hexdigest ( )
model_id = int ( model_hash [ : 13 ] , 16 )
# Standardized Castilian Note Template Structure Definition
spanish_note_model = genanki . Model (
model_id ,
' Castilian Audio Flashcard Model ' ,
fields = [
{ ' name ' : ' SpanishPhrase ' } ,
{ ' name ' : ' EnglishTranslation ' } ,
{ ' name ' : ' GrammarNotes ' } ,
{ ' name ' : ' AudioTrack ' }
] ,
templates = [
{
' name ' : ' Card 1: Auditory Identification ' ,
' qfmt ' : ' <div style= " font-family: Arial; font-size: 24px; text-align: center; color: #2c3e50; " > {{ SpanishPhrase}}</div><br><div style= " text-align: center; " > {{ AudioTrack}}</div> ' ,
' afmt ' : ' {{ FrontSide}}<hr id= " answer " ><div style= " font-family: Arial; font-size: 20px; text-align: center; color: #27ae60; font-weight: bold; " > {{ EnglishTranslation}}</div><br><div style= " font-family: Arial; font-size: 14px; text-align: center; color: #7f8c8d; font-style: italic; " > {{ GrammarNotes}}</div> ' ,
} ,
] ,
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 )
# Dictionary to store separate deck objects dynamically based on the DB assignments
decks_map = { }
media_files_manifest = [ ]
missing_assets_count = 0
# 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 """
SELECT t . deck_name , p1 . text , p2 . text , t . notes , t . tags , p1 . language
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 " "
lang = row [ 5 ]
# Reconstruct local media naming signature
safe_audio_name = " " . join ( [ c for c in es_text if c . isalnum ( ) or c in ( " " , " _ " ) ] ) . strip ( ) . replace ( " " , " _ " ) . lower ( )
relative_audio_path = f " media/ { safe_audio_name } _ { lang } _female.mp3 "
filename_only = f " { safe_audio_name } _ { lang } _female.mp3 "
# Check if target file exists. If missing, flag but don't crash
if os . path . exists ( relative_audio_path ) :
media_files_manifest . append ( relative_audio_path )
anki_audio_field = f " [sound: { filename_only } ] "
else :
missing_assets_count + = 1
anki_audio_field = " " # Fallback to empty if not generated yet
# 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 ]
# Construct unique note entry template matching properties
flash_note = genanki . Note (
model = spanish_note_model ,
fields = [ es_text , en_text , notes_text , anki_audio_field ] ,
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 )
success_msg = f " ✨ Packaged complete! \n \n File Output: { file_title } \n Destination: { self . anki_export_dir } \n Total Cards Built: { len ( records ) } \n Decks Created: { len ( decks_map ) } "
if missing_assets_count > 0 :
success_msg + = f " \n \n ⚠️ Note: { missing_assets_count } cards were bundled without audio tracks because their voice files hadn ' t been generated in the review window yet. "
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 )