2026-06-12 11:58:47 +00:00
# main.py
2026-06-14 09:56:43 +00:00
import sys
import os
from PyQt6 . QtWidgets import (
QApplication , QMainWindow , QWidget , QTabWidget , QVBoxLayout ,
QHBoxLayout , QLabel , QPushButton , QLineEdit , QComboBox ,
QTableWidget , QTableWidgetItem , QSlider , QFormLayout , QTextEdit , QFrame , QMessageBox
)
from PyQt6 . QtCore import Qt , QUrl
from PyQt6 . QtMultimedia import QMediaPlayer , QAudioOutput
from PyQt6 . QtGui import QFont
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 " )
self . setMinimumSize ( 1000 , 650 )
# 1. Initialize and Seed Database if Empty
self . ensure_database_populated ( )
# 2. Initialize PyQt Multimedia Audio Engine Components
self . media_player = QMediaPlayer ( )
self . audio_output = QAudioOutput ( )
self . media_player . setAudioOutput ( self . audio_output )
self . current_flashcard_id = None
# 3. Build UI Components
self . tabs = QTabWidget ( )
self . setCentralWidget ( self . tabs )
self . init_phrase_sandbox_tab ( )
self . init_flashcard_reviewer_tab ( )
# 4. Initial Load of Database Data into Grid View
self . refresh_crud_table ( )
def ensure_database_populated ( self ) :
""" Verifies if the database exists and has records; seeds it from the PDF if empty. """
init_db ( )
conn = get_connection ( )
cursor = conn . cursor ( )
try :
cursor . execute ( " SELECT COUNT(*) FROM phrases " )
count = cursor . fetchone ( ) [ 0 ]
except Exception :
count = 0
conn . close ( )
if count == 0 :
print ( " 🗄️ Database appears empty. Running the structural reconstruction pipeline... " )
pdf_file = " aula_int_plus_1_glos_en_alfa.pdf "
textbook = " Aula Internacional Plus 1 "
if os . path . exists ( pdf_file ) :
importer = BulkImporter ( )
importer . import_pdf_glossary ( pdf_file , textbook )
cleaner = GlossaryCleaner ( )
cleaner . process_database_clean ( )
print ( " ✨ Database successfully seeded with metadata-parsed entries. " )
else :
print ( f " ⚠️ Warning: Could not find ' { pdf_file } ' to automatically seed records. " )
# =====================================================================
# 🗄️ TAB 1: PHRASE SANDBOX (CRUD Panel)
# =====================================================================
def init_phrase_sandbox_tab ( self ) :
tab = QWidget ( )
layout = QHBoxLayout ( tab )
left_panel = QVBoxLayout ( )
search_layout = QHBoxLayout ( )
search_layout . addWidget ( QLabel ( " 🔍 Filter text: " ) )
self . search_input = QLineEdit ( )
self . search_input . textChanged . connect ( self . refresh_crud_table )
search_layout . addWidget ( self . search_input )
left_panel . addLayout ( search_layout )
self . phrase_table = QTableWidget ( )
self . phrase_table . setColumnCount ( 6 )
self . phrase_table . setHorizontalHeaderLabels ( [ " ID " , " Text " , " Lang " , " Type " , " Voice Gender " , " Deck Tag " ] )
self . phrase_table . itemSelectionChanged . connect ( self . handle_table_row_select )
left_panel . addWidget ( self . phrase_table )
right_panel = QVBoxLayout ( )
form_frame = QFrame ( )
form_frame . setFrameShape ( QFrame . Shape . StyledPanel )
form_layout = QFormLayout ( form_frame )
self . input_id = QLineEdit ( )
self . input_id . setReadOnly ( True )
self . input_id . setPlaceholderText ( " Auto-assigned ID " )
self . input_text = QTextEdit ( )
self . input_text . setMaximumHeight ( 60 )
self . combo_lang = QComboBox ( )
self . combo_lang . addItems ( [ " es " , " en " ] )
self . combo_type = QComboBox ( )
self . combo_type . addItems ( [ " sentence " , " phrase " , " noun " , " verb " , " adjective " ] )
self . combo_voice_gender = QComboBox ( )
self . combo_voice_gender . addItems ( [ " female " , " male " ] )
self . slider_base_speed = QSlider ( Qt . Orientation . Horizontal )
self . slider_base_speed . setMinimum ( 50 )
self . slider_base_speed . setMaximum ( 150 )
self . slider_base_speed . setValue ( 100 )
self . lbl_base_speed = QLabel ( " 1.00x " )
self . slider_base_speed . valueChanged . connect ( lambda v : self . lbl_base_speed . setText ( f " { v / 100 : .2f } x " ) )
speed_box = QHBoxLayout ( )
speed_box . addWidget ( self . slider_base_speed )
speed_box . addWidget ( self . lbl_base_speed )
self . input_deck_tag = QLineEdit ( )
self . input_deck_tag . setPlaceholderText ( " e.g., Anki_Unit_1 " )
form_layout . addRow ( " Phrase ID Resource: " , self . input_id )
form_layout . addRow ( QLabel ( " <b>Target Conversational Text:</b> " ) )
form_layout . addRow ( self . input_text )
form_layout . addRow ( " Language Accent: " , self . combo_lang )
form_layout . addRow ( " Grammar Type Classification: " , self . combo_type )
form_layout . addRow ( " <b>Preferred Voice Gender:</b> " , self . combo_voice_gender )
form_layout . addRow ( " <b>Default Playback Speed:</b> " , speed_box )
form_layout . addRow ( " Deck / Group Identifier Tag: " , self . input_deck_tag )
crud_buttons = QHBoxLayout ( )
self . btn_save = QPushButton ( " ➕ Save New" )
self . btn_update = QPushButton ( " 💾 Update Entry " )
self . btn_delete = QPushButton ( " 🗑️ Delete " )
self . btn_save . clicked . connect ( self . crud_create )
self . btn_update . clicked . connect ( self . crud_update )
self . btn_delete . clicked . connect ( self . crud_delete )
crud_buttons . addWidget ( self . btn_save )
crud_buttons . addWidget ( self . btn_update )
crud_buttons . addWidget ( self . btn_delete )
right_panel . addWidget ( QLabel ( " <h3>Configure Phrase & Voice Variables</h3> " ) )
right_panel . addWidget ( form_frame )
right_panel . addLayout ( crud_buttons )
right_panel . addStretch ( )
layout . addLayout ( left_panel , stretch = 3 )
layout . addLayout ( right_panel , stretch = 2 )
self . tabs . addTab ( tab , " 🗄️ Phrase Sandbox (CRUD) " )
# =====================================================================
# 🃏 TAB 2: FLASHCARD STUDY PLAYER
# =====================================================================
def init_flashcard_reviewer_tab ( self ) :
tab = QWidget ( )
layout = QVBoxLayout ( tab )
card_frame = QFrame ( )
card_frame . setStyleSheet ( " background-color: #ffffff; border: 2px solid #bdc3c7; border-radius: 12px; " )
card_layout = QVBoxLayout ( card_frame )
self . lbl_card_text = QLabel ( " Click ' Load Next ' to study sentences... " )
self . lbl_card_text . setAlignment ( Qt . AlignmentFlag . AlignCenter )
self . lbl_card_text . setFont ( QFont ( " Arial " , 22 , QFont . Weight . Bold ) )
self . lbl_card_text . setWordWrap ( True )
self . lbl_card_text . setStyleSheet ( " color: #2c3e50; border: none; padding: 20px; " )
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 ( )
playback_layout = QHBoxLayout ( )
playback_layout . addWidget ( QLabel ( " 🔊 Fine-Tune Study Speed: " ) )
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 )
self . lbl_review_speed = QLabel ( " 1.0x (Normal) " )
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 )
action_buttons = QHBoxLayout ( )
self . btn_play_voice = QPushButton ( " 🗣️ Play Voice Track " )
self . btn_flip_card = QPushButton ( " 👁️ Reveal Translation " )
self . btn_load_next = QPushButton ( " ➡️ Next Card " )
self . btn_play_voice . clicked . connect ( self . handle_play_voice )
self . btn_flip_card . clicked . connect ( self . handle_flip_card )
self . btn_load_next . clicked . connect ( self . handle_load_next_card )
action_buttons . addWidget ( self . btn_play_voice )
action_buttons . addWidget ( self . btn_flip_card )
action_buttons . addStretch ( )
action_buttons . addWidget ( self . btn_load_next )
layout . addWidget ( card_frame , stretch = 1 )
layout . addLayout ( playback_layout )
layout . addLayout ( action_buttons )
self . tabs . addTab ( tab , " 🃏 Flashcard Study " )
# =====================================================================
# ⚡ ENGINE BUSINESS LOGIC OPERATIONS & DATABASE MAPPINGS
# =====================================================================
2026-06-13 09:59:02 +00:00
2026-06-14 09:56:43 +00:00
def refresh_crud_table ( self ) :
conn = get_connection ( )
cursor = conn . cursor ( )
filter_text = self . search_input . text ( )
if filter_text :
cursor . execute ( """
SELECT id , text , language , word_type , voice_gender , deck_name
FROM phrases WHERE text LIKE ? ORDER BY id DESC LIMIT 100
""" , (f " % {filter_text} % " ,))
else :
cursor . execute ( """
SELECT id , text , language , word_type , voice_gender , deck_name
FROM phrases ORDER BY id DESC LIMIT 100
""" )
rows = cursor . fetchall ( )
conn . close ( )
self . phrase_table . setRowCount ( 0 )
for row_idx , row_data in enumerate ( rows ) :
self . phrase_table . insertRow ( row_idx )
for col_idx , value in enumerate ( row_data ) :
self . phrase_table . setItem ( row_idx , col_idx , QTableWidgetItem ( str ( value if value is not None else " " ) ) )
def handle_table_row_select ( self ) :
selected_ranges = self . phrase_table . selectedRanges ( )
if not selected_ranges :
return
row = selected_ranges [ 0 ] . topRow ( )
item = self . phrase_table . item ( row , 0 )
if not item :
return
phrase_id = item . text ( )
conn = get_connection ( )
cursor = conn . cursor ( )
cursor . execute ( " SELECT id, text, language, word_type, voice_gender, base_speed, deck_name FROM phrases WHERE id = ? " , ( phrase_id , ) )
record = cursor . fetchone ( )
conn . close ( )
if record :
self . input_id . setText ( str ( record [ 0 ] ) )
self . input_text . setPlainText ( str ( record [ 1 ] ) )
self . combo_lang . setCurrentText ( str ( record [ 2 ] ) )
self . combo_type . setCurrentText ( str ( record [ 3 ] ) if record [ 3 ] else " sentence " )
self . combo_voice_gender . setCurrentText ( str ( record [ 4 ] ) if record [ 4 ] else " female " )
speed_val = int ( ( record [ 5 ] if record [ 5 ] else 1.0 ) * 100 )
self . slider_base_speed . setValue ( speed_val )
self . input_deck_tag . setText ( str ( record [ 6 ] ) if record [ 6 ] else " General " )
def crud_create ( self ) :
conn = get_connection ( )
cursor = conn . cursor ( )
cursor . execute ( """
INSERT INTO phrases ( text , language , word_type , voice_gender , base_speed , deck_name )
VALUES ( ? , ? , ? , ? , ? , ? )
""" , (
self . input_text . toPlainText ( ) . strip ( ) ,
self . combo_lang . currentText ( ) ,
self . combo_type . currentText ( ) ,
self . combo_voice_gender . currentText ( ) ,
self . slider_base_speed . value ( ) / 100.0 , # Fixed typo: changed from .setValue() to .value()
self . input_deck_tag . text ( ) . strip ( ) or " General "
) )
conn . commit ( )
conn . close ( )
self . refresh_crud_table ( )
QMessageBox . information ( self , " Success " , " Phrase generated into database index store successfully. " )
def crud_update ( self ) :
pid = self . input_id . text ( )
if not pid :
return
conn = get_connection ( )
cursor = conn . cursor ( )
cursor . execute ( """
UPDATE phrases SET text = ? , language = ? , word_type = ? , voice_gender = ? , base_speed = ? , deck_name = ?
WHERE id = ?
""" , (
self . input_text . toPlainText ( ) . strip ( ) ,
self . combo_lang . currentText ( ) ,
self . combo_type . currentText ( ) ,
self . combo_voice_gender . currentText ( ) ,
self . slider_base_speed . value ( ) / 100.0 ,
self . input_deck_tag . text ( ) . strip ( ) or " General " ,
pid
) )
conn . commit ( )
conn . close ( )
self . refresh_crud_table ( )
QMessageBox . information ( self , " Success " , " Database record fields updated cleanly. " )
def crud_delete ( self ) :
pid = self . input_id . text ( )
if not pid :
return
conn = get_connection ( )
cursor = conn . cursor ( )
cursor . execute ( " DELETE FROM phrases WHERE id=? " , ( pid , ) )
conn . commit ( )
conn . close ( )
self . input_id . clear ( )
self . input_text . clear ( )
self . refresh_crud_table ( )
# =====================================================================
# 🔊 AUDIO PLAYER SYSTEM
# =====================================================================
def handle_load_next_card ( self ) :
conn = get_connection ( )
cursor = conn . cursor ( )
cursor . execute ( " SELECT id, text, language, word_type, voice_gender, base_speed FROM phrases ORDER BY RANDOM() LIMIT 1 " )
record = cursor . fetchone ( )
conn . close ( )
if record :
self . current_flashcard_id = record [ 0 ]
self . lbl_card_text . setText ( record [ 1 ] )
lang_lbl = " Spanish Accent " if record [ 2 ] == " es " else " English Accent "
gender_lbl = str ( record [ 4 ] ) . capitalize ( ) if record [ 4 ] else " Female "
self . lbl_card_meta . setText ( f " Classification: { record [ 3 ] } • Configured Voice: { lang_lbl } ( { gender_lbl } ) " )
card_saved_speed = int ( ( record [ 5 ] if record [ 5 ] else 1.0 ) * 100 )
self . slider_review_speed . setValue ( card_saved_speed )
def handle_play_voice ( self ) :
if not self . current_flashcard_id :
return
conn = get_connection ( )
cursor = conn . cursor ( )
cursor . execute ( " SELECT text, language, voice_gender FROM phrases WHERE id = ? " , ( self . current_flashcard_id , ) )
phrase_row = cursor . fetchone ( )
conn . close ( )
if not phrase_row :
return
text_str , lang_code , voice_gender = phrase_row
safe_filename = " " . join ( [ c for c in text_str if c . isalnum ( ) or c in ( " " , " _ " ) ] ) . rstrip ( ) . replace ( " " , " _ " ) . lower ( )
target_audio_file = f " media/ { safe_filename } _ { lang_code } _ { voice_gender } .mp3 "
os . makedirs ( " media " , exist_ok = True )
if not os . path . exists ( target_audio_file ) :
QMessageBox . warning ( self , " Audio Track Missing " ,
f " Audio track asset file not found in directory: \n ' { target_audio_file } ' \n \n Run the background Edge-TTS batch generator script next to download this track automatically. " )
return
self . media_player . setSource ( QUrl . fromLocalFile ( os . path . abspath ( target_audio_file ) ) )
current_rate = self . slider_review_speed . value ( ) / 100.0
self . media_player . setPlaybackRate ( current_rate )
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 )
def handle_flip_card ( self ) :
if not self . current_flashcard_id :
return
conn = get_connection ( )
cursor = conn . cursor ( )
cursor . execute ( """
SELECT p2 . text FROM translations t
JOIN phrases p2 ON t . target_phrase_id = p2 . id
WHERE t . source_phrase_id = ?
""" , (self.current_flashcard_id,))
row = cursor . fetchone ( )
conn . close ( )
if row :
current_es = self . lbl_card_text . text ( ) . split ( " \n \n 👉 " ) [ 0 ]
self . lbl_card_text . setText ( f " { current_es } \n \n 👉 [ { row [ 0 ] } ] " )
2026-06-12 11:58:47 +00:00
if __name__ == " __main__ " :
2026-06-14 09:56:43 +00:00
app = QApplication ( sys . argv )
window = SpanishTrainerApp ( )
window . show ( )
sys . exit ( app . exec ( ) )