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-20 01:55:05 +00:00
import subprocess
import json
import asyncio
import shutil
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-20 01:55:05 +00:00
# Third-Party Tooling
2026-06-19 05:30:59 +00:00
import genanki
2026-06-20 01:55:05 +00:00
import edge_tts
from PIL import Image , ImageDraw , ImageFont
2026-06-19 05:30:59 +00:00
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-20 01:55:05 +00:00
self . setMinimumSize ( 1200 , 800 )
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-20 01:55:05 +00:00
# Load system persistent settings from DB (including sleep-learning fields)
2026-06-19 05:09:05 +00:00
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-21 13:54:17 +00:00
# Flashcard Core State Variables
self . current_flashcard_id = None # Tracks the translation_id currently being reviewed
self . current_card_is_flipped = False # False = Front, True = Back
self . current_active_es_text = " " # Caches active Spanish string
self . current_active_en_text = " " # Caches active English string
self . flashcard_ids_pool = [ ] # Tracks currently filtered list of translation_ids
2026-06-20 01:55:05 +00:00
self . current_sandbox_es_id = None
self . current_sandbox_en_id = None
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-20 01:55:05 +00:00
# Ensure our settings table and key columns are structurally sound
2026-06-19 05:09:05 +00:00
cursor . execute ( " CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT); " )
2026-06-20 01:55:05 +00:00
# Seamlessly inject duration column into phrases if it doesn't exist
cursor . execute ( " PRAGMA table_info(phrases); " )
columns = [ row [ 1 ] for row in cursor . fetchall ( ) ]
if " duration " not in columns :
print ( " 🔄 Modifying phrases schema to support floating-point duration tracking... " )
cursor . execute ( " ALTER TABLE phrases ADD COLUMN duration REAL; " )
conn . commit ( )
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 ) :
2026-06-20 01:55:05 +00:00
""" Loads persistent variables from the key-value settings table. """
2026-06-19 05:09:05 +00:00
self . anki_export_dir = os . getcwd ( )
self . video_export_dir = os . getcwd ( )
2026-06-20 01:55:05 +00:00
self . video_first_lang = " English First (en -> es) "
self . video_repeats_count = " 3 "
self . video_pause_duration = " 4.0 "
2026-06-19 05:09:05 +00:00
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 ]
2026-06-20 01:55:05 +00:00
elif row [ 0 ] == " video_first_language " :
self . video_first_lang = row [ 1 ]
elif row [ 0 ] == " video_repeats_count " :
self . video_repeats_count = row [ 1 ]
elif row [ 0 ] == " video_pause_duration " :
self . video_pause_duration = row [ 1 ]
2026-06-19 05:09:05 +00:00
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 ( )
2026-06-20 01:55:05 +00:00
if key == " video_first_language " :
self . video_first_lang = value
elif key == " video_repeats_count " :
self . video_repeats_count = value
elif key == " video_pause_duration " :
self . video_pause_duration = value
2026-06-19 05:09:05 +00:00
except Exception as e :
print ( f " ❌ Critical: Failed to save setting ' { key } ' : { e } " )
finally :
conn . close ( )
2026-06-20 01:55:05 +00:00
def get_or_generate_audio_duration ( self , phrase_id , text_str , lang ) :
"""
Ensures a target audio track exists on disk , reads its run length
via ffprobe , caches the duration field inside SQLite , and returns the float timing block .
"""
2026-06-21 13:54:17 +00:00
if not text_str . strip ( ) :
2026-06-20 01:55:05 +00:00
return 2.5
safe_name = " " . join ( [ c for c in text_str if c . isalnum ( ) or c in ( " " , " _ " ) ] ) . strip ( ) . replace ( " " , " _ " ) . lower ( )
2026-06-21 13:54:17 +00:00
if not safe_name :
safe_name = hashlib . sha256 ( text_str . encode ( ' utf-8 ' ) ) . hexdigest ( ) [ : 16 ]
2026-06-20 01:55:05 +00:00
os . makedirs ( " media " , exist_ok = True )
target_file = f " media/ { safe_name } _ { lang } _female.mp3 "
if not os . path . exists ( target_file ) :
try :
voice = " es-ES-ElviraNeural " if lang == " es " else " en-GB-SoniaNeural "
communicate = edge_tts . Communicate ( text_str , voice )
asyncio . run ( communicate . save ( target_file ) )
except Exception as tts_err :
print ( f " ❌ Core TTS System Exception: { tts_err } " )
return 2.5
2026-06-21 13:54:17 +00:00
if phrase_id :
conn = get_connection ( )
cursor = conn . cursor ( )
cursor . execute ( " SELECT duration FROM phrases WHERE id = ? " , ( phrase_id , ) )
cached_row = cursor . fetchone ( )
if cached_row and cached_row [ 0 ] is not None :
conn . close ( )
return float ( cached_row [ 0 ] )
2026-06-20 01:55:05 +00:00
try :
cmd = [
' ffprobe ' , ' -v ' , ' quiet ' , ' -print_format ' , ' json ' ,
' -show_entries ' , ' format=duration ' , target_file
]
result = subprocess . run ( cmd , stdout = subprocess . PIPE , stderr = subprocess . PIPE , text = True )
data = json . loads ( result . stdout )
duration = float ( data [ ' format ' ] [ ' duration ' ] )
2026-06-21 13:54:17 +00:00
if phrase_id :
cursor . execute ( " UPDATE phrases SET duration = ? WHERE id = ? " , ( duration , phrase_id ) )
conn . commit ( )
2026-06-20 01:55:05 +00:00
except Exception as e :
print ( f " ⚠️ Track structure analysis warning for { target_file } : { e } " )
duration = 2.5
finally :
2026-06-21 13:54:17 +00:00
if phrase_id and ' conn ' in locals ( ) and conn :
conn . close ( )
2026-06-20 01:55:05 +00:00
return duration
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-21 13:54:17 +00:00
self . input_text_es . textChanged . connect ( self . clear_id_if_new_entry )
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-21 13:54:17 +00:00
self . input_text_en . textChanged . connect ( self . clear_id_if_new_entry )
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-21 13:54:17 +00:00
self . btn_flip_card = QPushButton ( " 👁️ Reveal Translation " )
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 )
2026-06-20 01:55:05 +00:00
self . combo_first_lang = QComboBox ( )
self . combo_first_lang . addItems ( [ " English First (en -> es) " , " Spanish First (es -> en) " ] )
self . combo_first_lang . setCurrentText ( self . video_first_lang )
self . combo_first_lang . currentTextChanged . connect ( lambda v : self . save_setting_to_db ( " video_first_language " , v ) )
self . spin_video_repeats = QLineEdit ( self . video_repeats_count )
self . spin_video_repeats . setFixedWidth ( 60 )
self . spin_video_repeats . textChanged . connect ( lambda v : self . save_setting_to_db ( " video_repeats_count " , v ) )
self . spin_pause_duration = QLineEdit ( self . video_pause_duration )
self . spin_pause_duration . setFixedWidth ( 60 )
self . spin_pause_duration . textChanged . connect ( lambda v : self . save_setting_to_db ( " video_pause_duration " , v ) )
2026-06-19 05:09:05 +00:00
form_layout . addRow ( " <b>Anki Deck Export Destination:</b> " , anki_layout )
form_layout . addRow ( " <b>Video Assembly Output Target:</b> " , video_layout )
2026-06-20 01:55:05 +00:00
form_layout . addRow ( " <b>Introductory Anchor Audio Language:</b> " , self . combo_first_lang )
form_layout . addRow ( " <b>Target Translation Loop Multiplier (Repeats):</b> " , self . spin_video_repeats )
form_layout . addRow ( " <b>User Recall Repetition Frame Intermission (Seconds):</b> " , self . spin_pause_duration )
self . btn_sync_cache = QPushButton ( " ⚡ Populate Audio & Timings Cache " )
self . btn_sync_cache . setStyleSheet ( """
QPushButton {
font - weight : bold ;
background - color : #e67e22;
color : white ;
padding : 10 px ;
border - radius : 5 px ;
font - size : 13 px ;
}
QPushButton : hover { background - color : #d35400; }
""" )
self . btn_sync_cache . clicked . connect ( self . handle_bulk_populate_audio_cache )
2026-06-19 05:09:05 +00:00
layout . addWidget ( QLabel ( " <h2>Application Preferences & Workspace Routing</h2> " ) )
layout . addWidget ( settings_frame )
2026-06-20 01:55:05 +00:00
layout . addWidget ( self . btn_sync_cache )
2026-06-19 05:09:05 +00:00
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-20 01:55:05 +00:00
def handle_bulk_populate_audio_cache ( self ) :
conn = get_connection ( )
cursor = conn . cursor ( )
cursor . execute ( """
SELECT p1 . id , p1 . text , p2 . id , p2 . text
FROM translations t
JOIN phrases p1 ON t . source_phrase_id = p1 . id
JOIN phrases p2 ON t . target_phrase_id = p2 . id
""" )
records = cursor . fetchall ( )
conn . close ( )
if not records :
QMessageBox . information ( self , " Cache Synchronizer " , " No valid translation pairs exist inside the database to process. " )
return
print ( f " ⚡ Processing structural cache updates for { len ( records ) } node linkages... " )
for row in records :
es_id , es_text , en_id , en_text = row [ 0 ] , row [ 1 ] . strip ( ) , row [ 2 ] , row [ 3 ] . strip ( )
self . get_or_generate_audio_duration ( en_id , en_text , " en " )
self . get_or_generate_audio_duration ( es_id , es_text , " es " )
QMessageBox . information ( self , " Cache Processing Complete " , " All missing speech segments successfully written. Timings cached safely. " )
2026-06-21 13:54:17 +00:00
# =====================================================================
# 💡 FLASHCARD OPERATIONS LOGIC COUPLING
# =====================================================================
def handle_live_speed_change ( self ) :
val = self . slider_review_speed . value ( )
self . lbl_review_speed . setText ( f " { val / 100 : .2f } x " )
def load_flashcard_by_id ( self , translation_id ) :
""" Loads a translation node into memory and targets local text widgets without notes clutter. """
conn = get_connection ( )
cursor = conn . cursor ( )
cursor . execute ( """
SELECT t . translation_id , p1 . text , p2 . text , p1 . source_context , t . tags , t . notes
FROM translations t
JOIN phrases p1 ON t . source_phrase_id = p1 . id
JOIN phrases p2 ON t . target_phrase_id = p2 . id
WHERE t . translation_id = ?
""" , (translation_id,))
record = cursor . fetchone ( )
conn . close ( )
if record :
self . current_flashcard_id = record [ 0 ]
self . current_active_es_text = str ( record [ 1 ] )
self . current_active_en_text = str ( record [ 2 ] )
self . current_card_is_flipped = False
self . lbl_card_text . setText ( self . current_active_en_text )
self . btn_flip_card . setText ( " 👁️ Reveal Translation " )
# --- t.notes (record[5]) has been intentionally omitted from this layout context ---
meta_str = f " Link ID: { record [ 0 ] } | Context: { record [ 3 ] or ' N/A ' } "
if record [ 4 ] :
meta_str + = f " | Tags: { record [ 4 ] } "
self . lbl_card_meta . setText ( meta_str )
self . handle_play_voice ( )
def handle_flip_card ( self ) :
if not self . current_flashcard_id :
return
if not self . current_card_is_flipped :
self . lbl_card_text . setText ( self . current_active_es_text )
self . btn_flip_card . setText ( " 👁️ Return to Prompt " )
self . current_card_is_flipped = True
else :
self . lbl_card_text . setText ( self . current_active_en_text )
self . btn_flip_card . setText ( " 👁️ Reveal Translation " )
self . current_card_is_flipped = False
def handle_play_voice ( self ) :
if not self . current_flashcard_id :
return
text_target = self . current_active_es_text if self . current_card_is_flipped else self . current_active_en_text
lang_target = " es " if self . current_card_is_flipped else " en "
speed_target = self . lbl_review_speed . text ( )
self . execute_playback ( text_target , lang_target , speed_target )
def handle_load_next_card ( self ) :
if not self . flashcard_ids_pool :
QMessageBox . information ( self , " Pool Empty " , " No flashcards found in the matrix matching current criteria filters. " )
return
next_tx_id = random . choice ( self . flashcard_ids_pool )
for row in range ( self . review_table . rowCount ( ) ) :
if int ( self . review_table . item ( row , 0 ) . text ( ) ) == next_tx_id :
self . review_table . setCurrentCell ( row , 0 )
break
self . load_flashcard_by_id ( next_tx_id )
# =====================================================================
# 📦 GENANKI EXPORT ENGINE (RESTRUCTURED PURE TRANSLATION FLOW)
# =====================================================================
def handle_export_anki_deck ( self ) :
"""
Gathers selected records from the matching criteria pool view , builds
a dual card template layout mapping English - > Spanish ( Card 1 ) and Spanish - > English
( Card 2 ) forward - reverse pairs cleanly . Explicitly maps 4 fields : EnglishText ,
EnglishAudio , SpanishText , SpanishAudio . Context and notes are fully removed .
"""
targets = self . flashcard_ids_pool
if not targets :
QMessageBox . warning ( self , " Export Aborted " , " The active flashcard pool filter is completely empty. Nothing to export. " )
return
conn = get_connection ( )
cursor = conn . cursor ( )
placeholders = " , " . join ( " ? " for _ in targets )
cursor . execute ( f """
SELECT t . translation_id , p1 . text , p2 . text , p1 . source_context , t . tags , t . notes , t . deck_name
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 IN ( { placeholders } )
""" , targets)
records = cursor . fetchall ( )
conn . close ( )
if not records :
QMessageBox . information ( self , " Export Processing " , " No structured database entries matched your criteria indices parameters. " )
return
# Unique Identification Anchor Codes for Anki Database Integrity
MODEL_ID = 1684321095
DECK_ID = 2026062011
# New Model structure aligning English Text/Audio with Spanish Text/Audio sequences
spanish_model = genanki . Model (
MODEL_ID ,
' Castilian Learning Model (Pure Text & Audio Alignment) ' ,
fields = [
{ ' name ' : ' EnglishText ' } ,
{ ' name ' : ' EnglishAudio ' } ,
{ ' name ' : ' SpanishText ' } ,
{ ' name ' : ' SpanishAudio ' }
] ,
templates = [
{
' name ' : ' Card 1: English -> Spanish ' ,
' qfmt ' : (
' <div style= " font-size: 13px; color: #2980b9; font-weight: bold; margin-bottom: 12px; letter-spacing: 1px; " >🇬🇧 ENGLISH COMPREHENSION</div> '
' <div style= " font-size: 25px; text-align: center; color: #2c3e50; font-family: Arial; " > {{ EnglishText}}</div> '
' <br><div style= " text-align:center; " > {{ EnglishAudio}}</div> '
) ,
' afmt ' : (
' {{ FrontSide}}<hr id= " answer " > '
' <div style= " font-size: 13px; color: #e74c3c; font-weight: bold; margin-bottom: 12px; letter-spacing: 1px; " >🇪🇸 SPANISH PRODUCTION</div> '
' <div style= " font-size: 25px; text-align: center; color: #16a085; font-family: Arial; font-weight: bold; " > {{ SpanishText}}</div> '
' <br><div style= " text-align:center; " > {{ SpanishAudio}}</div> '
) ,
} ,
{
' name ' : ' Card 2: Spanish -> English ' ,
' qfmt ' : (
' <div style= " font-size: 13px; color: #e74c3c; font-weight: bold; margin-bottom: 12px; letter-spacing: 1px; " >🇪🇸 SPANISH PRODUCTION</div> '
' <div style= " font-size: 25px; text-align: center; color: #2c3e50; font-family: Arial; " > {{ SpanishText}}</div> '
' <br><div style= " text-align:center; " > {{ SpanishAudio}}</div> '
) ,
' afmt ' : (
' {{ FrontSide}}<hr id= " answer " > '
' <div style= " font-size: 13px; color: #2980b9; font-weight: bold; margin-bottom: 12px; letter-spacing: 1px; " >🇬🇧 ENGLISH COMPREHENSION</div> '
' <div style= " font-size: 25px; text-align: center; color: #16a085; font-family: Arial; font-weight: bold; " > {{ EnglishText}}</div> '
' <br><div style= " text-align:center; " > {{ EnglishAudio}}</div> '
) ,
} ,
] ,
css = ' .card { font-family: arial; font-size: 20px; text-align: center; background-color: #fafafa; padding: 25px; border-radius: 8px; } '
)
deck_name_fallback = records [ 0 ] [ 6 ] if records [ 0 ] [ 6 ] else " Castilian Voice Trainer "
anki_deck = genanki . Deck ( DECK_ID , f " Spanish:: { deck_name_fallback } " )
media_files_bundle = [ ]
print ( f " 📦 Assembling Anki audio package for { len ( records ) } notes... " )
for row in records :
tx_id , es_text , en_text , context , tags , notes , deck_group = row
es_clean = es_text . strip ( )
en_clean = en_text . strip ( )
# --- Spanish Media Track Setup ---
safe_es_name = " " . join ( [ c for c in es_clean if c . isalnum ( ) or c in ( " " , " _ " ) ] ) . strip ( ) . replace ( " " , " _ " ) . lower ( )
if not safe_es_name :
safe_es_name = hashlib . sha256 ( es_clean . encode ( ' utf-8 ' ) ) . hexdigest ( ) [ : 16 ]
audio_es_filename = f " { safe_es_name } _es_female.mp3 "
full_es_path = f " media/ { audio_es_filename } "
if not os . path . exists ( full_es_path ) :
self . get_or_generate_audio_duration ( None , es_clean , " es " )
if os . path . exists ( full_es_path ) :
media_files_bundle . append ( full_es_path )
es_audio_tag = f " [sound: { audio_es_filename } ] "
else :
es_audio_tag = " "
# --- English Media Track Setup ---
safe_en_name = " " . join ( [ c for c in en_clean if c . isalnum ( ) or c in ( " " , " _ " ) ] ) . strip ( ) . replace ( " " , " _ " ) . lower ( )
if not safe_en_name :
safe_en_name = hashlib . sha256 ( en_clean . encode ( ' utf-8 ' ) ) . hexdigest ( ) [ : 16 ]
audio_en_filename = f " { safe_en_name } _en_female.mp3 "
full_en_path = f " media/ { audio_en_filename } "
if not os . path . exists ( full_en_path ) :
self . get_or_generate_audio_duration ( None , en_clean , " en " )
if os . path . exists ( full_en_path ) :
media_files_bundle . append ( full_en_path )
en_audio_tag = f " [sound: { audio_en_filename } ] "
else :
en_audio_tag = " "
# Meta and Category Tag Construction
tag_list = str ( tags ) . split ( ) if tags else [ ]
if context :
tag_list . append ( str ( context ) . replace ( " " , " _ " ) . replace ( " . " , " _ " ) )
# Populate note fields sequentially matching model specification:
# EnglishText, EnglishAudio, SpanishText, SpanishAudio
anki_note = genanki . Note (
model = spanish_model ,
fields = [
en_text ,
en_audio_tag ,
es_text ,
es_audio_tag
] ,
tags = tag_list
)
anki_deck . add_note ( anki_note )
# Output compilation assembly
export_output_path = os . path . join ( self . anki_export_dir , f " { deck_name_fallback . replace ( ' :: ' , ' _ ' ) } .apkg " )
package = genanki . Package ( anki_deck )
package . media_files = list ( set ( media_files_bundle ) ) # Excludes duplicate track instances
package . write_to_file ( export_output_path )
print ( f " ✅ Success! Balanced text-audio cards exported cleanly: { export_output_path } " )
QMessageBox . information (
self ,
" Anki Package Compiled " ,
f " Successfully compiled { len ( records ) } balanced text-audio translation flashcard nodes. \n \n Destination: \n { export_output_path } "
)
def handle_export_video_assets ( self ) :
pass
# =====================================================================
# ➕ CRUD ENGINE ATOMIC OPERATIONS LOGIC
# =====================================================================
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 ( """
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()))
tx_id = cursor . lastrowid
conn . commit ( )
conn . close ( )
self . input_tx_id . setText ( str ( tx_id ) )
self . current_sandbox_es_id = es_id
self . current_sandbox_en_id = en_id
self . refresh_crud_table ( )
self . refresh_review_table ( )
QMessageBox . information ( self , " Success " , f " Isolated phrase pairs created and bound to Translation ID { tx_id } . " )
def crud_update_pair ( self ) :
tx_id_str = self . input_tx_id . text ( ) . strip ( )
if not tx_id_str :
QMessageBox . warning ( self , " Update Target Missing " , " No Translation Link ID found. Select an existing record node or create a fresh link pair first. " )
return
if self . current_sandbox_es_id is None or self . current_sandbox_en_id is None :
QMessageBox . warning ( self , " Phrase Nodes Untracked " , " Underlying unique identifiers for individual language components are missing. Reselect the row from the left panel matrix grid. " )
return
conn = get_connection ( )
cursor = conn . cursor ( )
try :
cursor . execute ( """
UPDATE phrases
SET text = ? , word_type = ? , source_context = ? , duration = NULL
WHERE id = ?
""" , (self.input_text_es.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip(), self.current_sandbox_es_id))
cursor . execute ( """
UPDATE phrases
SET text = ? , word_type = ? , source_context = ? , duration = NULL
WHERE id = ?
""" , (self.input_text_en.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip(), self.current_sandbox_en_id))
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(), int(tx_id_str)))
conn . commit ( )
except Exception as e :
QMessageBox . critical ( self , " Database Error " , f " Failed to execute field modifications inside SQL engine: { e } " )
finally :
conn . close ( )
self . refresh_crud_table ( )
self . refresh_review_table ( )
QMessageBox . information ( self , " Success " , f " Node structural fields updated successfully. Translation Link ID { tx_id_str } remains active. " )
def crud_delete_pair ( self ) :
pass
def execute_playback ( self , text_str , lang , speed_text ) :
txt = text_str . strip ( )
if not txt :
return
self . get_or_generate_audio_duration ( None , txt , lang )
safe_name = " " . join ( [ c for c in txt if c . isalnum ( ) or c in ( " " , " _ " ) ] ) . strip ( ) . replace ( " " , " _ " ) . lower ( )
if not safe_name :
safe_name = hashlib . sha256 ( txt . encode ( ' utf-8 ' ) ) . hexdigest ( ) [ : 16 ]
target_file = f " media/ { safe_name } _ { lang } _female.mp3 "
if os . path . exists ( target_file ) :
try :
multiplier = float ( speed_text . replace ( " x " , " " ) )
except ValueError :
multiplier = 1.0
self . media_player . stop ( )
self . media_player . setSource ( QUrl . fromLocalFile ( os . path . abspath ( target_file ) ) )
self . media_player . setLoops ( 1 )
self . media_player . setPlaybackRate ( multiplier )
self . media_player . play ( )
def handle_sandbox_play_es ( self ) :
self . execute_playback ( self . input_text_es . toPlainText ( ) , " es " , self . combo_speed_es . currentText ( ) )
def handle_sandbox_play_en ( self ) :
self . execute_playback ( self . input_text_en . toPlainText ( ) , " en " , self . combo_speed_en . currentText ( ) )
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 = """
2026-06-21 13:54:17 +00:00
SELECT t . translation_id , p1 . text , p1 . source_context , t . tags
2026-06-19 03:42:28 +00:00
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-21 13:54:17 +00:00
self . flashcard_ids_pool . append ( row_data [ 0 ] )
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-20 01:55:05 +00:00
SELECT t . translation_id , p1 . text , p2 . text , p1 . word_type , p1 . source_context , t . deck_name , t . notes , t . tags , p1 . id , p2 . id
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-20 01:55:05 +00:00
self . current_sandbox_es_id = record [ 8 ]
self . current_sandbox_en_id = record [ 9 ]
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 ( )
2026-06-21 13:54:17 +00:00
if row < len ( self . flashcard_ids_pool ) :
translation_id = self . flashcard_ids_pool [ row ]
self . load_flashcard_by_id ( translation_id )
2026-06-19 03:42:28 +00:00
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-21 13:54:17 +00:00
def clear_id_if_new_entry ( self ) :
if self . input_tx_id . text ( ) and not ( self . input_text_es . hasFocus ( ) or self . input_text_en . hasFocus ( ) ) :
2026-06-19 03:42:28 +00:00
pass
2026-06-12 11:58:47 +00:00
if __name__ == " __main__ " :
2026-06-21 13:54:17 +00:00
app = QApplication ( sys . argv )
app . setApplicationName ( " Spanish Voice Trainer " )
app . setOrganizationName ( " Oxnee Pty. Ltd. " )
window = SpanishTrainerApp ( )
window . show ( )
sys . exit ( app . exec ( ) )