before redesign of the GUI

This commit is contained in:
stephen 2026-06-14 19:56:43 +10:00
parent ae1c382c3c
commit b3104d66a5
3 changed files with 530 additions and 134 deletions

View file

@ -4,59 +4,93 @@ from database.connection import get_connection
class GlossaryCleaner: class GlossaryCleaner:
def __init__(self): def __init__(self):
# Captures trailing markers: " m", " f", " m, pl", " f, pl", " pl"
self.gender_pattern = re.compile(r'\s+\b(m|f|m,\s*pl|f,\s*pl|pl)\b\s*$')
# Captures verb irregular brackets: " (zc)", " (ie)", etc. # Captures verb irregular brackets: " (zc)", " (ie)", etc.
self.verb_pattern = re.compile(r'\s*\(([a-z\s]+)\)') self.verb_pattern = re.compile(r'\s*\(([a-z\s]+)\)')
def extract_metadata(self, text: str) -> tuple[str, str, str]: # Aggressive character-level match for broken trailing gender tags
""" # Tracks variations like: " m", " f", "mpl", "fpl", "smpl", "osmpl" at the end of a string
Parses text to isolate the clean conversational string, self.broken_gender_pattern = re.compile(r'[\s]*\b(m|f|pl)\b$|[\s\w]*(m|f|pl|mpl|fpl|smpl|osmpl)$', re.IGNORECASE)
the word classification type, and specific grammatical notes.
""" def clean_and_expand_spanish(self, text: str) -> tuple[list[str], str, str]:
"""Parses and strips layout noise from Spanish strings, extracting rich metadata."""
word_type = "phrase" word_type = "phrase"
grammar_note = None grammar_note = None
clean_text = text.strip() clean_text = text.strip()
# Check for verb present-tense irregular markers # 1. Extract verb irregularities if present
verb_match = self.verb_pattern.search(clean_text) verb_match = self.verb_pattern.search(clean_text)
if verb_match: if verb_match:
word_type = "verb" word_type = "verb"
grammar_note = verb_match.group(1) # e.g., 'zc', 'ie' grammar_note = verb_match.group(1).strip()
clean_text = self.verb_pattern.sub('', clean_text).strip() clean_text = self.verb_pattern.sub('', clean_text).strip()
return clean_text, word_type, grammar_note
# Check for noun gender indicators # 2. Extract and remove sticky gender notations (e.g., "bañ osmpl" -> "baños")
gender_match = self.gender_pattern.search(clean_text) # Check if text ends with common markers
if gender_match: lower_text = clean_text.lower()
if lower_text.endswith(('mpl', 'fpl', 'smpl', 'osmpl')):
word_type = "noun" word_type = "noun"
grammar_note = gender_match.group(1).strip() # e.g., 'm', 'f' if 'f' in lower_text:
clean_text = self.gender_pattern.sub('', clean_text).strip() grammar_note = "f, pl"
return clean_text, word_type, grammar_note else:
grammar_note = "m, pl"
# If it's a single word without tags, check if it's likely an adjective/noun split # Reconstruct the original word base before the layout break
if ' ' not in clean_text and '/' in clean_text: # e.g., "bañ osmpl" -> remove "osmpl" and append "os" to restore "baños"
if lower_text.endswith('osmpl') and clean_text.lower().endswith('osmpl'):
clean_text = clean_text[:-5] + "os"
else:
# General strip of trailing garbage characters
clean_text = re.sub(r'\s*[a-zA-Z\s]*$', '', clean_text)
# Standard boundary check for clean tags (e.g., "años60 m")
elif re.search(r'\s+\b(m|f|pl|m,\s*pl|f,\s*pl)\b\s*$', clean_text):
word_type = "noun"
match = re.search(r'\b(m|f|pl|m,\s*pl|f,\s*pl)\b\s*$', clean_text)
if match:
grammar_note = match.group(1).strip()
clean_text = re.sub(r'\s+\b(m|f|pl|m,\s*pl|f,\s*pl)\b\s*$', '', clean_text).strip()
clean_text = clean_text.strip()
# 3. Expand dual-gender adjectives (e.g., "apasionado/a")
variants = []
if '/' in clean_text and ' ' not in clean_text:
word_type = "adjective" word_type = "adjective"
base, suffix = clean_text.split('/', 1)
base = base.strip()
suffix = suffix.strip()
return clean_text, word_type, grammar_note if suffix == 'a' and base.endswith('o'):
variants.append((base, word_type, "m"))
variants.append((base[:-1] + 'a', word_type, "f"))
elif suffix == 'ra' and base.endswith('r'):
variants.append((base, word_type, "m"))
variants.append((base + 'a', word_type, "f"))
else:
variants.append((clean_text, word_type, grammar_note))
else:
variants.append((clean_text, word_type, grammar_note))
def ensure_columns_exist(self, cursor): return variants
"""Dynamically appends schema metadata columns to phrases table if missing."""
try: def clean_english_text(self, text: str) -> str:
cursor.execute("ALTER TABLE phrases ADD COLUMN word_type TEXT") """Fixes layout spacing issues on English infinitive verbs."""
cursor.execute("ALTER TABLE phrases ADD COLUMN grammar_note TEXT") cleaned = text.strip()
except Exception: # Ensure duplicate internal spacing is compressed
# Columns already exist, skip safe alert safely cleaned = re.sub(r'\s+', ' ', cleaned)
pass
# Intercept smashed English infinitives (e.g., "toappear" -> "to appear")
if cleaned.startswith("to") and len(cleaned) > 2 and not cleaned.startswith("to "):
cleaned = re.sub(r'^to([a-z])', r'to \1', cleaned)
return cleaned
def process_database_clean(self): def process_database_clean(self):
"""Processes and normalizes raw table entries into pristine structures."""
conn = get_connection() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
# Ensure our rich metadata slots exist in SQLite print("🧹 Extracting raw dataset for deep metadata extraction...")
self.ensure_columns_exist(cursor)
print("🧹 Extracting raw glossary dataset for metadata preservation...")
cursor.execute(""" cursor.execute("""
SELECT t.source_phrase_id, p1.text as es_text, p2.text as en_text, SELECT t.source_phrase_id, p1.text as es_text, p2.text as en_text,
@ -73,9 +107,9 @@ class GlossaryCleaner:
conn.close() conn.close()
return return
print(f"⚙️ Migrating {len(raw_rows)} rows into a structured format...") print(f"⚙️ Migrating {len(raw_rows)} rows into corrected schemas...")
# Clear out current tables to run a fresh, structured reload # Clear out previous passes completely
cursor.execute("DELETE FROM translations") cursor.execute("DELETE FROM translations")
cursor.execute("DELETE FROM phrases") cursor.execute("DELETE FROM phrases")
cursor.execute("DELETE FROM audio_tracks") cursor.execute("DELETE FROM audio_tracks")
@ -83,44 +117,28 @@ class GlossaryCleaner:
inserted_count = 0 inserted_count = 0
for _, es_raw, en_raw, textbook, unit, context in raw_rows: for _, es_raw, en_raw, textbook, unit, context in raw_rows:
# Isolate text from technical indicators # Process Spanish layout text elements
clean_es, word_type, grammar_note = self.extract_metadata(es_raw) es_variants = self.clean_and_expand_spanish(es_raw)
# Process and restore spaces to English text elements
en_clean = self.clean_english_text(en_raw)
# Handle dual-gender expansions like "apasionado/a" for es_clean, w_type, g_note in es_variants:
variants = []
if '/' in clean_es:
base, suffix = clean_es.split('/', 1)
base = base.strip()
suffix = suffix.strip()
if suffix == 'a' and base.endswith('o'):
variants.append((base, "adjective", "m"))
variants.append((base[:-1] + 'a', "adjective", "f"))
elif suffix == 'ra' and base.endswith('r'):
variants.append((base, "adjective", "m"))
variants.append((base + 'a', "adjective", "f"))
else:
variants.append((clean_es, word_type, grammar_note))
else:
variants.append((clean_es, word_type, grammar_note))
for es_variant, w_type, g_note in variants:
try: try:
# 1. Insert Spanish entry with structural metadata columns filled # Insert pristine Spanish entry
cursor.execute(""" cursor.execute("""
INSERT INTO phrases (text, language, textbook, unit, source_context, word_type, grammar_note) INSERT INTO phrases (text, language, textbook, unit, source_context, word_type, grammar_note)
VALUES (?, 'es', ?, ?, ?, ?, ?) VALUES (?, 'es', ?, ?, ?, ?, ?)
""", (es_variant, textbook, unit, context, w_type, g_note)) """, (es_clean, textbook, unit, context, w_type, g_note))
es_id = cursor.lastrowid es_id = cursor.lastrowid
# 2. Insert English entry # Insert spaced English entry
cursor.execute(""" cursor.execute("""
INSERT INTO phrases (text, language, textbook, unit, source_context, word_type, grammar_note) INSERT INTO phrases (text, language, textbook, unit, source_context, word_type, grammar_note)
VALUES (?, 'en', ?, ?, ?, ?, NULL) VALUES (?, 'en', ?, ?, ?, ?, NULL)
""", (en_raw, textbook, unit, context, w_type)) """, (en_clean, textbook, unit, context, w_type))
en_id = cursor.lastrowid en_id = cursor.lastrowid
# 3. Create Bidirectional Cross-References # Re-map relations
cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (es_id, en_id)) cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (es_id, en_id))
cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (en_id, es_id)) cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (en_id, es_id))
@ -130,4 +148,4 @@ class GlossaryCleaner:
conn.commit() conn.commit()
conn.close() conn.close()
print(f"🎉 Metadata-aware normalization complete! Saved {inserted_count} structured phrases.") print(f"🎉 Schema extraction pipeline completed! Saved {inserted_count} pristine phrase definitions.")

View file

@ -1,59 +1,54 @@
# database/connection.py # database/connection.py
import sqlite3 import sqlite3
import os
DB_NAME = "spanish_trainer.db"
def get_connection(): def get_connection():
"""Returns a standard connection object to the SQLite database.""" return sqlite3.connect("spanish_trainer.db")
return sqlite3.connect(DB_NAME)
def init_db(): def init_db():
"""
Initializes the SQLite database tables if they do not exist.
This safely runs on every boot without wiping your existing data.
"""
print(f"🗄️ Checking database status for '{DB_NAME}'...")
# The SQL schema we designed for your glossary, cross-references, and tracks
schema = """
CREATE TABLE IF NOT EXISTS phrases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
language TEXT NOT NULL,
textbook TEXT DEFAULT NULL,
unit INTEGER DEFAULT NULL,
source_context TEXT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS translations (
source_phrase_id INTEGER,
target_phrase_id INTEGER,
PRIMARY KEY (source_phrase_id, target_phrase_id),
FOREIGN KEY (source_phrase_id) REFERENCES phrases(id) ON DELETE CASCADE,
FOREIGN KEY (target_phrase_id) REFERENCES phrases(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS audio_tracks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
phrase_id INTEGER NOT NULL,
voice_gender TEXT NOT NULL,
voice_name TEXT NOT NULL,
file_path TEXT NOT NULL,
is_reference INTEGER DEFAULT 1,
FOREIGN KEY (phrase_id) REFERENCES phrases(id) ON DELETE CASCADE
);
"""
conn = get_connection() conn = get_connection()
try: cursor = conn.cursor()
cursor = conn.cursor()
# executescript allows running multiple CREATE TABLE statements at once # 1. Main Phrases Table
cursor.executescript(schema) cursor.execute("""
conn.commit() CREATE TABLE IF NOT EXISTS phrases (
print("✅ Database tables verified and initialized successfully.") id INTEGER PRIMARY KEY AUTOINCREMENT,
except sqlite3.Error as e: text TEXT NOT NULL,
print(f"❌ Database initialization failed: {e}") language TEXT NOT NULL,
finally: textbook TEXT,
conn.close() unit INTEGER,
source_context TEXT,
word_type TEXT,
grammar_note TEXT,
voice_gender TEXT DEFAULT 'female',
base_speed REAL DEFAULT 1.0,
deck_name TEXT DEFAULT 'General',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# 2. Translations Cross-Reference Table
cursor.execute("""
CREATE TABLE IF NOT EXISTS translations (
source_phrase_id INTEGER,
target_phrase_id INTEGER,
PRIMARY KEY (source_phrase_id, target_phrase_id),
FOREIGN KEY (source_phrase_id) REFERENCES phrases(id) ON DELETE CASCADE,
FOREIGN KEY (target_phrase_id) REFERENCES phrases(id) ON DELETE CASCADE
)
""")
# 3. Audio Tracks Metadata Table (Required by the glossary cleaner)
cursor.execute("""
CREATE TABLE IF NOT EXISTS audio_tracks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
phrase_id INTEGER,
file_path TEXT NOT NULL,
sample_rate INTEGER,
duration REAL,
FOREIGN KEY (phrase_id) REFERENCES phrases(id) ON DELETE CASCADE
)
""")
conn.commit()
conn.close()
print("✅ Rich metadata database schema initialized successfully.")

415
main.py
View file

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