From 5312eb75be87dd5db70dbfb86342f7fd548e38d7 Mon Sep 17 00:00:00 2001 From: stephen Date: Fri, 19 Jun 2026 13:42:28 +1000 Subject: [PATCH] modification of Flashcard Review tab --- database/trainer_backup_20260618_224811.db | Bin 0 -> 4096 bytes main.py | 374 +++++++++++++-------- 2 files changed, 242 insertions(+), 132 deletions(-) create mode 100644 database/trainer_backup_20260618_224811.db diff --git a/database/trainer_backup_20260618_224811.db b/database/trainer_backup_20260618_224811.db new file mode 100644 index 0000000000000000000000000000000000000000..b5f3f030a33b05c8aa8882423b1e2038dc3b35cd GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}R))P*7lCU|@t|AO!{>KB<6_L9h1*FG!aFsai+X qkA}c#2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S0iD+B;^69<(5 literal 0 HcmV?d00001 diff --git a/main.py b/main.py index a4f392d..5a5c90f 100644 --- a/main.py +++ b/main.py @@ -19,7 +19,7 @@ class SpanishTrainerApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Castilian Voice Trainer Pro") - self.setMinimumSize(1150, 700) + self.setMinimumSize(1200, 750) # 1. Initialize schema structures and check ingestion status self.ensure_database_populated() @@ -27,10 +27,10 @@ class SpanishTrainerApp(QMainWindow): # Audio Player Architecture Setup self.media_player = QMediaPlayer() self.audio_output = QAudioOutput() - - # Set the audio output routing to the player instance 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() @@ -39,8 +39,9 @@ class SpanishTrainerApp(QMainWindow): self.init_phrase_sandbox_tab() self.init_flashcard_reviewer_tab() - # 2. Populate unified database rows into interface layout grid + # 2. Populate both 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.""" @@ -50,7 +51,6 @@ class SpanishTrainerApp(QMainWindow): conn = get_connection() cursor = conn.cursor() - # Verify if the translations table has rows populated try: cursor.execute("SELECT COUNT(*) FROM translations") count = cursor.fetchone()[0] @@ -66,11 +66,9 @@ class SpanishTrainerApp(QMainWindow): pdf_file = "aula_int_plus_1_glos_en_alfa.pdf" if os.path.exists(pdf_file): - # Run the layout parser staging execution pass importer = BulkImporter() importer.import_pdf_glossary(pdf_file, "Aula Internacional Plus 1") - # Clean, pair up English/Spanish, and insert the final rows cleaner = GlossaryCleaner() cleaner.process_database_clean() print("✨ Ingestion pipeline processing sequence successfully completed.") @@ -84,7 +82,6 @@ class SpanishTrainerApp(QMainWindow): tab = QWidget() layout = QHBoxLayout(tab) - # --- LEFT SIDE PANEL: Filter Controls & Unified Data Grid --- left_panel = QVBoxLayout() filter_layout = QHBoxLayout() @@ -96,14 +93,13 @@ class SpanishTrainerApp(QMainWindow): 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.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) - # Unified Translations Row Table Matrix self.translation_table = QTableWidget() self.translation_table.setColumnCount(6) self.translation_table.setHorizontalHeaderLabels([ @@ -112,7 +108,6 @@ class SpanishTrainerApp(QMainWindow): 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") @@ -122,7 +117,6 @@ class SpanishTrainerApp(QMainWindow): nav_layout.addWidget(self.btn_row_down) left_panel.addLayout(nav_layout) - # --- RIGHT SIDE PANEL: Side-by-Side Unified Twin Form Box Views --- right_panel = QVBoxLayout() form_frame = QFrame() form_frame.setFrameShape(QFrame.Shape.StyledPanel) @@ -144,14 +138,12 @@ class SpanishTrainerApp(QMainWindow): self.input_context = QLineEdit() self.input_context.setPlaceholderText("e.g., U8_5A") - # Explicit Tags field implementation self.input_tags = QLineEdit() - self.input_tags.setPlaceholderText("e.g., irregular_er boots_verb (space-separated)") + self.input_tags.setPlaceholderText("e.g., irregular_er boots_verb") self.input_deck_tag = QLineEdit() self.input_deck_tag.setPlaceholderText("Anki Sub-deck Hierarchy") - # Common layout stylesheet for the utility buttons button_qss = """ QPushButton { background-color: #f0f0f0; @@ -170,7 +162,6 @@ class SpanishTrainerApp(QMainWindow): } """ - # šŸ”Š 1. Spanish Header Layout with Speed Controls es_header_layout = QHBoxLayout() es_header_layout.setContentsMargins(0, 5, 0, 5) es_header_layout.addWidget(QLabel("šŸ‡ŖšŸ‡ø Castilian Spanish Text Element:")) @@ -190,7 +181,6 @@ class SpanishTrainerApp(QMainWindow): 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("šŸ‡¬šŸ‡§ English Target Translation:")) @@ -210,7 +200,6 @@ class SpanishTrainerApp(QMainWindow): en_header_layout.addWidget(self.combo_speed_en) en_header_layout.addStretch() - # šŸ“ 3. Symmetrical Grammar/Usage Note Header Layout grammar_header_layout = QHBoxLayout() grammar_header_layout.setContentsMargins(0, 5, 0, 5) grammar_header_layout.addWidget(QLabel("šŸ“ Grammar / Usage Note:")) @@ -218,9 +207,8 @@ class SpanishTrainerApp(QMainWindow): self.input_grammar_note = QTextEdit() self.input_grammar_note.setMaximumHeight(75) - self.input_grammar_note.setPlaceholderText("e.g., feminine variant, irregular radical change, requires subjunctive...") + self.input_grammar_note.setPlaceholderText("e.g., feminine variant...") - # Mount elements sequentially into the form frame mapping structure form_layout.addRow("Translation Link ID:", self.input_tx_id) form_layout.addRow(es_header_layout) form_layout.addRow(self.input_text_es) @@ -257,21 +245,52 @@ class SpanishTrainerApp(QMainWindow): self.tabs.addTab(tab, "šŸ—„ļø Phrase Sandbox (CRUD)") # ===================================================================== - # šŸƒ TAB 2: FLASHCARD STUDY MODULE + # šŸƒ TAB 2: FLASHCARD STUDY MODULE (UPGRADED TWIN FRAME WINDOW) # ===================================================================== def init_flashcard_reviewer_tab(self): tab = QWidget() - layout = QVBoxLayout(tab) + layout = QHBoxLayout(tab) + # --- LEFT SIDE PANEL: Symmetrical Review Filters & Substack Table --- + 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 SIDE PANEL: Workspace Review Controls & Active Layout Flashcard Canvas --- + right_panel = QVBoxLayout() + + # Large Display Flashcard Frame Window Canvas (Top Half Block) 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 'Next Card' to initiate study sequence...") + 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", 22, QFont.Weight.Bold)) + 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: 25px;") + self.lbl_card_text.setStyleSheet("color: #2c3e50; border: none; padding: 20px;") self.lbl_card_meta = QLabel("") self.lbl_card_meta.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -282,40 +301,61 @@ class SpanishTrainerApp(QMainWindow): card_layout.addWidget(self.lbl_card_text) card_layout.addWidget(self.lbl_card_meta) card_layout.addStretch() + right_panel.addWidget(card_frame, stretch=4) + # Middle Operational Playback Stack Controls playback_layout = QHBoxLayout() - playback_layout.addWidget(QLabel("šŸ”Š Voice Track Speed:")) + 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.0x") + 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_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) + right_panel.addLayout(action_buttons) - layout.addWidget(card_frame, stretch=1) - layout.addLayout(playback_layout) - layout.addLayout(action_buttons) + right_panel.addSpacing(15) + # Bottom Utility Navigation & Deployment Array + 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) + + # Style deployment array distinctively + 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") # ===================================================================== - # ⚔ ENGINE DATABASE LOGIC & FILTER COMPILATIONS + # ⚔ DATA MATRIX CONTROL & SYNCHRONIZATION VIEWS # ===================================================================== def refresh_crud_table(self): """Pulls unified translation nodes into pairs while enforcing context text matches.""" @@ -326,7 +366,7 @@ class SpanishTrainerApp(QMainWindow): 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, t.notes + 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 @@ -355,6 +395,44 @@ class SpanishTrainerApp(QMainWindow): 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): + """Pulls and displays a targeted subset stack matching specific context or tag filters.""" + 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]) # Track active selection text key reference IDs + 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: @@ -388,30 +466,38 @@ class SpanishTrainerApp(QMainWindow): 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): - """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) + # ===================================================================== + # āž• ENGINE ATOMIC OPERATIONS LOGIC (CRUD MODIFIERS) + # ===================================================================== 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 complete with tags data 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())) @@ -419,6 +505,7 @@ class SpanishTrainerApp(QMainWindow): 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): @@ -433,7 +520,6 @@ class SpanishTrainerApp(QMainWindow): 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=?", @@ -444,6 +530,7 @@ class SpanishTrainerApp(QMainWindow): conn.close() self.refresh_crud_table() + self.refresh_review_table() QMessageBox.information(self, "Success", "Relational node structural update complete.") def crud_delete_pair(self): @@ -462,6 +549,7 @@ class SpanishTrainerApp(QMainWindow): 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() @@ -469,111 +557,76 @@ class SpanishTrainerApp(QMainWindow): self.input_tags.clear() # ===================================================================== - # šŸ”Š AUDIO OPERATIONS & FLASHCARD CONTROL + # šŸ”Š AUDIO ENGINE & EXPANDED FLASHCARD ACTIONS # ===================================================================== - 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.") - 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): - 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 (" ", "_")]).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): - 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() - - def handle_load_next_card(self): - """Picks a random phrase node from the database, shifting state context to flashcard mode.""" + def load_flashcard_by_id(self, phrase_id): + """Sets internal execution contexts cleanly targeting a specific card reference.""" 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 + 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.language = 'es' - ORDER BY RANDOM() LIMIT 1 - """) + WHERE p1.id = ? + """, (phrase_id,)) record = cursor.fetchone() conn.close() if record: - self.current_flashcard_id = record[5] + self.current_flashcard_id = record[4] self.lbl_card_text.setText(record[1]) - self.lbl_card_meta.setText(f"Tx Link node: {record[0]} • Context: {record[3]} • Subdeck: {record[4]}") + 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): + """Picks a random track row selection pulled directly from the current filtered list pool.""" + 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) + + # Highlight matching row inside left panel layout matrix for context tracking + 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): + """Plays or generates the Castilian neural track dynamically on fallback request loops.""" 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,)) + cursor.execute("SELECT text, language FROM phrases WHERE id = ?", (self.current_flashcard_id,)) row = cursor.fetchone() conn.close() if row: - text_str, lang, gender = 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" - resolved_gender = gender if gender else "female" - target_file = f"media/{safe_name}_{lang}_{resolved_gender}.mp3" + # Fault-Tolerance Loop: If Sandbox pass missed this track, generate it seamlessly right here + 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() - else: - QMessageBox.warning(self, "Asset Missing", f"Audio file not found at path location:\n{target_file}") - - 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: @@ -592,13 +645,81 @@ class SpanishTrainerApp(QMainWindow): if row: clean_es = self.lbl_card_text.text().split("\n\nšŸ‘‰")[0] display_text = f"{clean_es}\n\nšŸ‘‰ [ {row[0]} ]" - - # If a note exists for the card, append it dynamically to the reveal layout 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 placeholder loop for your upcoming genanki package deployment modules.""" + QMessageBox.information( + self, "Anki Export Engine", + f"Staging packaging manifest for active view subset!\n\n" + f"Currently Filtered Scope count: {len(self.flashcard_ids_pool)} cards.\n" + f"Target Destination output: Castilian_Spanish_Workspace.apkg" + ) + + def handle_export_video_assets(self): + """Action handler placeholder loop for compiling video cards.""" + QMessageBox.information( + self, "Video Synthesis Suite", + f"Staging visual timeline render frames loop!\n\n" + f"Currently Filtered Scope count: {len(self.flashcard_ids_pool)} sequences.\n" + f"Target Audio Tracks bound: edge-tts neural assets map." + ) + # ===================================================================== # šŸš€ DIAGNOSTIC STARTUP FRAMEWORK WRAPPER # ===================================================================== @@ -606,22 +727,11 @@ 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) \ No newline at end of file