139_spanish-voice-trainer/main.py
2026-06-19 15:09:05 +10:00

816 lines
No EOL
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# main.py
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, QFileDialog
)
from PyQt6.QtCore import Qt, QUrl
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
from PyQt6.QtGui import QFont
# Internal Project Module Imports
from database.connection import init_db, get_connection
from core.bulk_importer import BulkImporter
from core.clean_glossary import GlossaryCleaner
class SpanishTrainerApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Castilian Voice Trainer Pro")
self.setMinimumSize(1200, 750)
# 1. Initialize schema structures and check ingestion status
self.ensure_database_populated()
# Load system persistent settings from DB
self.load_system_settings()
# Audio Player Architecture Setup
self.media_player = QMediaPlayer()
self.audio_output = QAudioOutput()
self.media_player.setAudioOutput(self.audio_output)
self.current_flashcard_id = None
self.flashcard_ids_pool = [] # Tracks currently filtered study list IDs
# Central Main Window Tabs Interface
self.tabs = QTabWidget()
self.setCentralWidget(self.tabs)
self.init_phrase_sandbox_tab()
self.init_flashcard_reviewer_tab()
self.init_settings_tab() # Mount the new settings panel
# 2. Populate table grids on initialization
self.refresh_crud_table()
self.refresh_review_table()
def ensure_database_populated(self):
"""Forces database configuration structure and triggers pipeline execution if empty."""
print("🗄️ Verification Pass: Running schema configuration scripts...")
init_db()
conn = get_connection()
cursor = conn.cursor()
# Ensure the settings table exists alongside legacy core tables
cursor.execute("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT);")
try:
cursor.execute("SELECT COUNT(*) FROM translations")
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}")
count = 0
finally:
conn.close()
if count == 0:
print("🗄️ Database tables are empty. Triggering glossary reader pipeline...")
pdf_file = "aula_int_plus_1_glos_en_alfa.pdf"
if os.path.exists(pdf_file):
importer = BulkImporter()
importer.import_pdf_glossary(pdf_file, "Aula Internacional Plus 1")
cleaner = GlossaryCleaner()
cleaner.process_database_clean()
print("✨ Ingestion pipeline processing sequence successfully completed.")
else:
print(f"❌ Error: Source document '{pdf_file}' is missing from the directory root.")
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()
# =====================================================================
# 🗄️ TAB 1: TRANSLATION-CENTRIC PHRASE SANDBOX (CRUD)
# =====================================================================
def init_phrase_sandbox_tab(self):
tab = QWidget()
layout = QHBoxLayout(tab)
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)
filter_layout.addWidget(QLabel("📂 Context:"))
self.search_context_input = QLineEdit()
self.search_context_input.setPlaceholderText("e.g. U2")
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)
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")
self.input_text_es = QTextEdit()
self.input_text_es.setMaximumHeight(75)
self.input_text_en = QTextEdit()
self.input_text_en.setMaximumHeight(75)
self.combo_type = QComboBox()
self.combo_type.addItems(["phrase", "sentence", "noun", "verb", "adjective"])
self.input_context = QLineEdit()
self.input_context.setPlaceholderText("e.g., U8_5A")
self.input_tags = QLineEdit()
self.input_tags.setPlaceholderText("e.g., irregular_er boots_verb")
self.input_deck_tag = QLineEdit()
self.input_deck_tag.setPlaceholderText("Anki Sub-deck Hierarchy")
button_qss = """
QPushButton {
background-color: #f0f0f0;
border: 1px solid #c0c0c0;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
color: #333333;
}
QPushButton:hover {
background-color: #e0e0e0;
border: 1px solid #a0a0a0;
}
QPushButton:pressed {
background-color: #d0d0d0;
}
"""
es_header_layout = QHBoxLayout()
es_header_layout.setContentsMargins(0, 5, 0, 5)
es_header_layout.addWidget(QLabel("<b>🇪🇸 Castilian Spanish Text Element:</b>"))
self.btn_play_sandbox_es = QPushButton("Play 🔊")
self.btn_play_sandbox_es.setFixedWidth(75)
self.btn_play_sandbox_es.setFixedHeight(24)
self.btn_play_sandbox_es.setStyleSheet(button_qss)
self.btn_play_sandbox_es.clicked.connect(self.handle_sandbox_play_es)
es_header_layout.addWidget(self.btn_play_sandbox_es)
self.combo_speed_es = QComboBox()
self.combo_speed_es.addItems(["0.50x", "0.75x", "1.00x", "1.25x", "1.50x"])
self.combo_speed_es.setCurrentText("1.00x")
self.combo_speed_es.setFixedWidth(70)
self.combo_speed_es.setFixedHeight(24)
es_header_layout.addWidget(self.combo_speed_es)
es_header_layout.addStretch()
en_header_layout = QHBoxLayout()
en_header_layout.setContentsMargins(0, 5, 0, 5)
en_header_layout.addWidget(QLabel("<b>🇬🇧 English Target Translation:</b>"))
self.btn_play_sandbox_en = QPushButton("Play 🔊")
self.btn_play_sandbox_en.setFixedWidth(75)
self.btn_play_sandbox_en.setFixedHeight(24)
self.btn_play_sandbox_en.setStyleSheet(button_qss)
self.btn_play_sandbox_en.clicked.connect(self.handle_sandbox_play_en)
en_header_layout.addWidget(self.btn_play_sandbox_en)
self.combo_speed_en = QComboBox()
self.combo_speed_en.addItems(["0.50x", "0.75x", "1.00x", "1.25x", "1.50x"])
self.combo_speed_en.setCurrentText("1.00x")
self.combo_speed_en.setFixedWidth(70)
self.combo_speed_en.setFixedHeight(24)
en_header_layout.addWidget(self.combo_speed_en)
en_header_layout.addStretch()
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)
self.input_grammar_note.setPlaceholderText("e.g., feminine variant...")
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(grammar_header_layout)
form_layout.addRow(self.input_grammar_note)
form_layout.addRow("Classification Profile:", self.combo_type)
form_layout.addRow("Source Context ID (Raw):", self.input_context)
form_layout.addRow("Anki Note Tags:", self.input_tags)
form_layout.addRow("<b>Target Deck Scope:</b>", self.input_deck_tag)
crud_buttons = QHBoxLayout()
self.btn_save = QPushButton(" Create Pair")
self.btn_update = QPushButton("💾 Update Node")
self.btn_delete = QPushButton("🗑️ Sever Link")
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)
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>"))
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)
self.tabs.addTab(tab, "🗄️ Phrase Sandbox (CRUD)")
# =====================================================================
# 🃏 TAB 2: FLASHCARD STUDY MODULE
# =====================================================================
def init_flashcard_reviewer_tab(self):
tab = QWidget()
layout = QHBoxLayout(tab)
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()
card_frame = QFrame()
card_frame.setStyleSheet("background-color: #ffffff; border: 2px solid #bdc3c7; border-radius: 12px;")
card_layout = QVBoxLayout(card_frame)
card_frame.setMinimumHeight(280)
self.lbl_card_text = QLabel("Select a row or click 'Next Card' to initiate...")
self.lbl_card_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.lbl_card_text.setFont(QFont("Arial", 20, 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()
right_panel.addWidget(card_frame, stretch=4)
playback_layout = QHBoxLayout()
playback_layout.addWidget(QLabel("🔊 Voice 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.00x")
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)
right_panel.addLayout(playback_layout)
action_buttons = QHBoxLayout()
self.btn_play_voice = QPushButton("🗣️ Play Voice Track")
self.btn_flip_card = QPushButton("👁️ Reveal English Partner")
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)
right_panel.addLayout(action_buttons)
right_panel.addSpacing(15)
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)
self.tabs.addTab(tab, "🃏 Flashcard Review")
# =====================================================================
# ⚙️ 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 Export Path Form Group
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 Export Path Form Group
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):
"""Triggers a file browser directory selection for native Anki card compiles."""
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):
"""Triggers a file browser directory selection for video timeline outputs."""
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)
# =====================================================================
# ⚡ DATA MATRIX CONTROL & SYNCHRONIZATION VIEWS
# =====================================================================
def refresh_crud_table(self):
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}%"])
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)
rows = cursor.fetchall()
conn.close()
self.translation_table.setRowCount(0)
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 "")))
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)
self.flashcard_ids_pool.append(row_data[4])
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 "")))
def handle_table_row_select(self):
selected_ranges = self.translation_table.selectedRanges()
if not selected_ranges:
return
row = selected_ranges[0].topRow()
tx_id_item = self.translation_table.item(row, 0)
if not tx_id_item:
return
tx_id = tx_id_item.text()
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, t.notes, t.tags
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,))
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_tags.setText(str(record[7]) if record[7] is not None else "")
self.input_deck_tag.setText(str(record[5]) if record[5] else "General")
self.input_grammar_note.setPlainText(str(record[6]) if record[6] is not None else "")
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)
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)
# =====================================================================
# ENGINE ATOMIC OPERATIONS LOGIC (CRUD MODIFIERS)
# =====================================================================
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()))
conn.commit()
conn.close()
self.refresh_crud_table()
self.refresh_review_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
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("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=?, 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))
conn.commit()
conn.close()
self.refresh_crud_table()
self.refresh_review_table()
QMessageBox.information(self, "Success", "Relational node structural update complete.")
def crud_delete_pair(self):
tx_id = self.input_tx_id.text()
if not tx_id:
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()
conn.close()
self.refresh_crud_table()
self.refresh_review_table()
self.input_tx_id.clear()
self.input_text_es.clear()
self.input_text_en.clear()
self.input_grammar_note.clear()
self.input_tags.clear()
# =====================================================================
# 🔊 AUDIO ENGINE & EXPANDED FLASHCARD ACTIONS
# =====================================================================
def load_flashcard_by_id(self, phrase_id):
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT t.translation_id, p1.text, p1.source_context, t.deck_name, p1.id, t.tags
FROM translations t
JOIN phrases p1 ON t.source_phrase_id = p1.id
WHERE p1.id = ?
""", (phrase_id,))
record = cursor.fetchone()
conn.close()
if record:
self.current_flashcard_id = record[4]
self.lbl_card_text.setText(record[1])
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
import random
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)
def handle_play_voice(self):
if not self.current_flashcard_id:
return
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT text, language FROM phrases WHERE id = ?", (self.current_flashcard_id,))
row = cursor.fetchone()
conn.close()
if row:
text_str, lang = row
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}_{lang}_female.mp3"
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
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()
def handle_flip_card(self):
if not self.current_flashcard_id:
return
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT p2.text, 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 p1.id = ?
""", (self.current_flashcard_id,))
row = cursor.fetchone()
conn.close()
if row:
clean_es = self.lbl_card_text.text().split("\n\n👉")[0]
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)
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)
# =====================================================================
# 📦 ARTIFACT EXPORT GATEWAYS (ANKI & DEPLOYMENT CODES)
# =====================================================================
def handle_export_anki_deck(self):
"""Action handler loop for genanki package deployment modules."""
QMessageBox.information(
self, "Anki Export Engine",
f"Staging packaging manifest for active view subset!\n\n"
f"Target Directory: {self.anki_export_dir}\n"
f"Currently Filtered Scope count: {len(self.flashcard_ids_pool)} cards."
)
def handle_export_video_assets(self):
"""Action handler loop for compiling video cards."""
QMessageBox.information(
self, "Video Synthesis Suite",
f"Staging visual timeline render frames loop!\n\n"
f"Target Directory: {self.video_export_dir}\n"
f"Currently Filtered Scope count: {len(self.flashcard_ids_pool)} sequences."
)
# =====================================================================
# 🚀 DIAGNOSTIC STARTUP FRAMEWORK WRAPPER
# =====================================================================
if __name__ == "__main__":
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()
sys.exit(1)