139_spanish-voice-trainer/main.py

599 lines
26 KiB
Python
Raw Permalink Normal View History

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
# 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")
self.setMinimumSize(1150, 700)
2026-06-14 09:56:43 +00:00
# 1. Initialize schema structures and check ingestion status
2026-06-14 09:56:43 +00:00
self.ensure_database_populated()
# 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)
self.current_flashcard_id = None
# 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()
# 2. Populate unified database rows into interface layout grid
2026-06-14 09:56:43 +00:00
self.refresh_crud_table()
def ensure_database_populated(self):
"""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()
# Verify if the translations table has rows populated
2026-06-14 09:56:43 +00:00
try:
cursor.execute("SELECT COUNT(*) FROM translations")
2026-06-14 09:56:43 +00:00
count = cursor.fetchone()[0]
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
finally:
conn.close()
2026-06-14 09:56:43 +00:00
if count == 0:
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):
# Run the layout parser staging execution pass
2026-06-14 09:56:43 +00:00
importer = BulkImporter()
importer.import_pdf_glossary(pdf_file, "Aula Internacional Plus 1")
2026-06-14 09:56:43 +00:00
# Clean, pair up English/Spanish, and insert the final rows
2026-06-14 09:56:43 +00:00
cleaner = GlossaryCleaner()
cleaner.process_database_clean()
print("✨ Ingestion pipeline processing sequence successfully completed.")
2026-06-14 09:56:43 +00:00
else:
print(f"❌ Error: Source document '{pdf_file}' is missing from the directory root.")
2026-06-14 09:56:43 +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 SIDE PANEL: Filter Controls & Unified Data Grid ---
2026-06-14 09:56:43 +00:00
left_panel = QVBoxLayout()
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
filter_layout.addWidget(QLabel("📂 Context:"))
self.search_context_input = QLineEdit()
self.search_context_input.setPlaceholderText("e.g. U2 or U8_5A")
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)
# Unified Translations Row Table Matrix
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)
# Row Pointer Navigation Steppers
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)
# --- RIGHT SIDE PANEL: Side-by-Side Unified Twin Form Box Views ---
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)
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
self.input_text_es = QTextEdit()
self.input_text_es.setMaximumHeight(75)
2026-06-14 09:56:43 +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()
self.combo_type.addItems(["phrase", "sentence", "noun", "verb", "adjective"])
2026-06-14 09:56:43 +00:00
self.input_context = QLineEdit()
self.input_context.setPlaceholderText("e.g., U8_5A")
2026-06-14 09:56:43 +00:00
self.input_deck_tag = QLineEdit()
self.input_deck_tag.setPlaceholderText("Anki Sub-deck Hierarchy")
2026-06-14 09:56:43 +00:00
# Common layout stylesheet for the utility buttons
button_qss = """
QPushButton {
background-color: #f0f0f0;
border: 1px solid #c0c0c0;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
color: #333333;
}
QPushButton:hover {
background-color: #e0e0e0;
border: 1px solid #a0a0a0;
}
QPushButton:pressed {
background-color: #d0d0d0;
}
"""
# 🔊 1. Spanish Header Layout with Speed Controls
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()
# 🔊 2. English Header Layout Activated with Speed Controls
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()
# Mount elements sequentially into the form frame mapping structure
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)
form_layout.addRow("Classification Profile:", self.combo_type)
form_layout.addRow("Source Context ID (Raw):", self.input_context)
form_layout.addRow("<b>Target Deck Scope:</b>", self.input_deck_tag)
2026-06-14 09:56:43 +00:00
crud_buttons = QHBoxLayout()
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
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)
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()
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)")
# =====================================================================
# 🃏 TAB 2: FLASHCARD STUDY MODULE
2026-06-14 09:56:43 +00:00
# =====================================================================
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("Select 'Next Card' to initiate study sequence...")
2026-06-14 09:56:43 +00:00
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: 25px;")
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()
playback_layout = QHBoxLayout()
playback_layout.addWidget(QLabel("🔊 Voice Track 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)
self.lbl_review_speed = QLabel("1.0x")
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)
action_buttons = QHBoxLayout()
self.btn_play_voice = QPushButton("🗣️ Play Voice Track")
self.btn_flip_card = QPushButton("👁️ Reveal English Partner")
2026-06-14 09:56:43 +00:00
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 Review")
2026-06-14 09:56:43 +00:00
# =====================================================================
# ⚡ ENGINE DATABASE LOGIC & FILTER COMPILATIONS
2026-06-14 09:56:43 +00:00
# =====================================================================
def refresh_crud_table(self):
"""Pulls unified translation nodes into pairs while enforcing context text matches."""
2026-06-14 09:56:43 +00:00
conn = get_connection()
cursor = conn.cursor()
text_filter = self.search_text_input.text().strip()
context_filter = self.search_context_input.text().strip()
query = """
SELECT t.translation_id, p1.text, p2.text, p1.word_type, p1.source_context, 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 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
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()
self.translation_table.setRowCount(0)
2026-06-14 09:56:43 +00:00
for row_idx, row_data in enumerate(rows):
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
def handle_table_row_select(self):
selected_ranges = self.translation_table.selectedRanges()
2026-06-14 09:56:43 +00:00
if not selected_ranges:
return
row = selected_ranges[0].topRow()
tx_id_item = self.translation_table.item(row, 0)
if not tx_id_item:
2026-06-14 09:56:43 +00:00
return
tx_id = tx_id_item.text()
2026-06-14 09:56:43 +00:00
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT t.translation_id, p1.text, p2.text, p1.word_type, p1.source_context, 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 = ?
""", (tx_id,))
2026-06-14 09:56:43 +00:00
record = cursor.fetchone()
conn.close()
if record:
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 "")
self.input_deck_tag.setText(str(record[5]) if record[5] else "General")
def step_table_row(self, direction):
"""Steps your focus row highlighting pointer index sequentially."""
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)
def crud_create_pair(self):
conn = get_connection()
cursor = conn.cursor()
# Write Spanish node entry
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
# Write English node entry
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
# Build cross-referencing translation relational binding matrix row
cursor.execute("""
INSERT INTO translations (source_phrase_id, target_phrase_id, deck_name) VALUES (?, ?, ?)
""", (es_id, en_id, self.input_deck_tag.text().strip() or "General"))
conn.commit()
conn.close()
self.refresh_crud_table()
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()
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
# Keep underscores exact in context updates
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))
cursor.execute("UPDATE translations SET deck_name=? WHERE translation_id=?",
(self.input_deck_tag.text().strip() or "General", tx_id))
conn.commit()
2026-06-14 09:56:43 +00:00
conn.close()
self.refresh_crud_table()
QMessageBox.information(self, "Success", "Relational node structural update complete.")
2026-06-14 09:56:43 +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()
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()
self.input_tx_id.clear()
self.input_text_es.clear()
self.input_text_en.clear()
2026-06-14 09:56:43 +00:00
# =====================================================================
# 🔊 AUDIO OPERATIONS & FLASHCARD CONTROL
# =====================================================================
def handle_sandbox_play_es(self):
"""Generates/plays the Spanish phrase from the Sandbox using the chosen speed rate."""
text_str = self.input_text_es.toPlainText().strip()
if not text_str:
QMessageBox.warning(self, "Empty Value", "Please select a valid translation pair or type a Spanish phrase to play.")
2026-06-14 09:56:43 +00:00
return
safe_name = "".join([c for c in text_str if c.isalnum() or c in (" ", "_")]).rstrip().replace(" ", "_").lower()
os.makedirs("media", exist_ok=True)
target_file = f"media/{safe_name}_es_female.mp3"
if not os.path.exists(target_file):
print(f"🔊 Generating Neural Castilian Spanish track 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"Failed to synthesize Spanish voice:\n{tts_err}")
return
if os.path.exists(target_file):
speed_multiplier = float(self.combo_speed_es.currentText().replace("x", ""))
self.media_player.setSource(QUrl.fromLocalFile(os.path.abspath(target_file)))
self.media_player.setPlaybackRate(speed_multiplier)
self.media_player.play()
def handle_sandbox_play_en(self):
"""Generates/plays the English translation string from the Sandbox using its speed rate."""
text_str = self.input_text_en.toPlainText().strip()
if not text_str:
QMessageBox.warning(self, "Empty Value", "Please enter text inside the English target translation container.")
return
safe_name = "".join([c for c in text_str if c.isalnum() or c in (" ", "_")]).rstrip().replace(" ", "_").lower()
os.makedirs("media", exist_ok=True)
target_file = f"media/{safe_name}_en_female.mp3"
if not os.path.exists(target_file):
print(f"🔊 Generating Neural English voice track for '{text_str}'...")
try:
import asyncio
import edge_tts
communicate = edge_tts.Communicate(text_str, "en-GB-SoniaNeural")
asyncio.run(communicate.save(target_file))
except Exception as tts_err:
QMessageBox.critical(self, "TTS Error", f"Failed to synthesize English voice:\n{tts_err}")
return
if os.path.exists(target_file):
speed_multiplier = float(self.combo_speed_en.currentText().replace("x", ""))
self.media_player.setSource(QUrl.fromLocalFile(os.path.abspath(target_file)))
self.media_player.setPlaybackRate(speed_multiplier)
self.media_player.play()
2026-06-14 09:56:43 +00:00
def handle_load_next_card(self):
"""Picks a random phrase node from the database, shifting state context to flashcard mode."""
2026-06-14 09:56:43 +00:00
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT t.translation_id, p1.text, p1.word_type, p1.source_context, t.deck_name, p1.id
FROM translations t
JOIN phrases p1 ON t.source_phrase_id = p1.id
WHERE p1.language = 'es'
ORDER BY RANDOM() LIMIT 1
""")
2026-06-14 09:56:43 +00:00
record = cursor.fetchone()
conn.close()
if record:
self.current_flashcard_id = record[5]
2026-06-14 09:56:43 +00:00
self.lbl_card_text.setText(record[1])
self.lbl_card_meta.setText(f"Tx Link node: {record[0]} • Context: {record[3]} • Subdeck: {record[4]}")
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()
cursor.execute("SELECT text, language, voice_gender FROM phrases WHERE id = ?", (self.current_flashcard_id,))
row = cursor.fetchone()
2026-06-14 09:56:43 +00:00
conn.close()
if row:
text_str, lang, gender = row
safe_name = "".join([c for c in text_str if c.isalnum() or c in (" ", "_")]).rstrip().replace(" ", "_").lower()
2026-06-14 09:56:43 +00:00
resolved_gender = gender if gender else "female"
target_file = f"media/{safe_name}_{lang}_{resolved_gender}.mp3"
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()
else:
QMessageBox.warning(self, "Asset Missing", f"Audio file not found at path location:\n{target_file}")
2026-06-14 09:56:43 +00:00
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 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
WHERE p1.id = ?
2026-06-14 09:56:43 +00:00
""", (self.current_flashcard_id,))
row = cursor.fetchone()
conn.close()
if row:
clean_es = self.lbl_card_text.text().split("\n\n👉")[0]
self.lbl_card_text.setText(f"{clean_es}\n\n👉 [ {row[0]} ]")
2026-06-12 11:58:47 +00:00
# =====================================================================
# 🚀 DIAGNOSTIC STARTUP FRAMEWORK WRAPPER
# =====================================================================
2026-06-12 11:58:47 +00:00
if __name__ == "__main__":
print("🚀 Initializing PyQt6 Application Framework...")
try:
app = QApplication(sys.argv)
print("🔧 Spawning SpanishTrainerApp Instance...")
window = SpanishTrainerApp()
print("🖥️ Mounting User Interface Windows...")
window.show()
print("🎯 Event loop engaged. Handing over control thread...")
sys.exit(app.exec())
except Exception as fatal_error:
import traceback
print("\n❌ CRITICAL CRASH DETECTED ON CORE STARTUP THREAD!")
print("====================================================")
print(f"Error Type: {type(fatal_error).__name__}")
print(f"Error Message: {fatal_error}")
print("====================================================")
traceback.print_exc()
sys.exit(1)