Compare commits
No commits in common. "main" and "v14.0" have entirely different histories.
6 changed files with 784 additions and 1690 deletions
0
database/trainer.db
Normal file
0
database/trainer.db
Normal file
1650
doc/Notes.md
1650
doc/Notes.md
File diff suppressed because it is too large
Load diff
BIN
doc/Notes.pdf
BIN
doc/Notes.pdf
Binary file not shown.
824
main.py
824
main.py
|
|
@ -1,12 +1,6 @@
|
|||
# main.py
|
||||
import sys
|
||||
import os
|
||||
import random
|
||||
import hashlib
|
||||
import subprocess
|
||||
import json
|
||||
import asyncio
|
||||
import shutil
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout,
|
||||
QHBoxLayout, QLabel, QPushButton, QLineEdit, QComboBox,
|
||||
|
|
@ -16,11 +10,6 @@ from PyQt6.QtCore import Qt, QUrl
|
|||
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
|
||||
from PyQt6.QtGui import QFont
|
||||
|
||||
# Third-Party Tooling
|
||||
import genanki
|
||||
import edge_tts
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
# Internal Project Module Imports
|
||||
from database.connection import init_db, get_connection
|
||||
from core.bulk_importer import BulkImporter
|
||||
|
|
@ -30,12 +19,12 @@ class SpanishTrainerApp(QMainWindow):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Castilian Voice Trainer Pro")
|
||||
self.setMinimumSize(1200, 800)
|
||||
self.setMinimumSize(1200, 750)
|
||||
|
||||
# 1. Initialize schema structures and check ingestion status
|
||||
self.ensure_database_populated()
|
||||
|
||||
# Load system persistent settings from DB (including sleep-learning fields)
|
||||
# Load system persistent settings from DB
|
||||
self.load_system_settings()
|
||||
|
||||
# Audio Player Architecture Setup
|
||||
|
|
@ -43,15 +32,8 @@ class SpanishTrainerApp(QMainWindow):
|
|||
self.audio_output = QAudioOutput()
|
||||
self.media_player.setAudioOutput(self.audio_output)
|
||||
|
||||
# Flashcard Core State Variables
|
||||
self.current_flashcard_id = None # Tracks the translation_id currently being reviewed
|
||||
self.current_card_is_flipped = False # False = Front, True = Back
|
||||
self.current_active_es_text = "" # Caches active Spanish string
|
||||
self.current_active_en_text = "" # Caches active English string
|
||||
self.flashcard_ids_pool = [] # Tracks currently filtered list of translation_ids
|
||||
|
||||
self.current_sandbox_es_id = None
|
||||
self.current_sandbox_en_id = None
|
||||
self.current_flashcard_id = None
|
||||
self.flashcard_ids_pool = [] # Tracks currently filtered study list IDs
|
||||
|
||||
# Central Main Window Tabs Interface
|
||||
self.tabs = QTabWidget()
|
||||
|
|
@ -59,7 +41,7 @@ class SpanishTrainerApp(QMainWindow):
|
|||
|
||||
self.init_phrase_sandbox_tab()
|
||||
self.init_flashcard_reviewer_tab()
|
||||
self.init_settings_tab()
|
||||
self.init_settings_tab() # Mount the new settings panel
|
||||
|
||||
# 2. Populate table grids on initialization
|
||||
self.refresh_crud_table()
|
||||
|
|
@ -73,17 +55,9 @@ class SpanishTrainerApp(QMainWindow):
|
|||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Ensure our settings table and key columns are structurally sound
|
||||
# Ensure the settings table exists alongside legacy core tables
|
||||
cursor.execute("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT);")
|
||||
|
||||
# Seamlessly inject duration column into phrases if it doesn't exist
|
||||
cursor.execute("PRAGMA table_info(phrases);")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
if "duration" not in columns:
|
||||
print("🔄 Modifying phrases schema to support floating-point duration tracking...")
|
||||
cursor.execute("ALTER TABLE phrases ADD COLUMN duration REAL;")
|
||||
conn.commit()
|
||||
|
||||
try:
|
||||
cursor.execute("SELECT COUNT(*) FROM translations")
|
||||
count = cursor.fetchone()[0]
|
||||
|
|
@ -109,13 +83,9 @@ class SpanishTrainerApp(QMainWindow):
|
|||
print(f"❌ Error: Source document '{pdf_file}' is missing from the directory root.")
|
||||
|
||||
def load_system_settings(self):
|
||||
"""Loads persistent variables from the key-value settings table."""
|
||||
"""Loads persistent path directories from the key-value settings table."""
|
||||
self.anki_export_dir = os.getcwd()
|
||||
self.video_export_dir = os.getcwd()
|
||||
self.video_first_lang = "English First (en -> es)"
|
||||
self.video_repeats_count = "3"
|
||||
self.video_pause_duration = "4.0"
|
||||
self.default_deck_name = "Trainer" # Unified structural configuration fallback
|
||||
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
|
@ -127,14 +97,6 @@ class SpanishTrainerApp(QMainWindow):
|
|||
self.anki_export_dir = row[1]
|
||||
elif row[0] == "video_export_directory":
|
||||
self.video_export_dir = row[1]
|
||||
elif row[0] == "video_first_language":
|
||||
self.video_first_lang = row[1]
|
||||
elif row[0] == "video_repeats_count":
|
||||
self.video_repeats_count = row[1]
|
||||
elif row[0] == "video_pause_duration":
|
||||
self.video_pause_duration = row[1]
|
||||
elif row[0] == "default_deck_name":
|
||||
self.default_deck_name = row[1]
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to read application settings from database: {e}")
|
||||
finally:
|
||||
|
|
@ -147,75 +109,11 @@ class SpanishTrainerApp(QMainWindow):
|
|||
try:
|
||||
cursor.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", (key, value))
|
||||
conn.commit()
|
||||
|
||||
if key == "video_first_language":
|
||||
self.video_first_lang = value
|
||||
elif key == "video_repeats_count":
|
||||
self.video_repeats_count = value
|
||||
elif key == "video_pause_duration":
|
||||
self.video_pause_duration = value
|
||||
elif key == "default_deck_name":
|
||||
self.default_deck_name = value
|
||||
except Exception as e:
|
||||
print(f"❌ Critical: Failed to save setting '{key}': {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_or_generate_audio_duration(self, phrase_id, text_str, lang):
|
||||
"""
|
||||
Ensures a target audio track exists on disk, reads its run length
|
||||
via ffprobe, caches the duration field inside SQLite, and returns the float timing block.
|
||||
"""
|
||||
if not text_str.strip():
|
||||
return 2.5
|
||||
|
||||
safe_name = "".join([c for c in text_str if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
||||
if not safe_name:
|
||||
safe_name = hashlib.sha256(text_str.encode('utf-8')).hexdigest()[:16]
|
||||
|
||||
os.makedirs("media", exist_ok=True)
|
||||
target_file = f"media/{safe_name}_{lang}_female.mp3"
|
||||
|
||||
if not os.path.exists(target_file):
|
||||
try:
|
||||
voice = "es-ES-ElviraNeural" if lang == "es" else "en-GB-SoniaNeural"
|
||||
communicate = edge_tts.Communicate(text_str, voice)
|
||||
asyncio.run(communicate.save(target_file))
|
||||
except Exception as tts_err:
|
||||
print(f"❌ Core TTS System Exception: {tts_err}")
|
||||
return 2.5
|
||||
|
||||
if phrase_id:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT duration FROM phrases WHERE id = ?", (phrase_id,))
|
||||
cached_row = cursor.fetchone()
|
||||
|
||||
if cached_row and cached_row[0] is not None:
|
||||
conn.close()
|
||||
return float(cached_row[0])
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
'ffprobe', '-v', 'quiet', '-print_format', 'json',
|
||||
'-show_entries', 'format=duration', target_file
|
||||
]
|
||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
data = json.loads(result.stdout)
|
||||
duration = float(data['format']['duration'])
|
||||
|
||||
if phrase_id:
|
||||
cursor.execute("UPDATE phrases SET duration = ? WHERE id = ?", (duration, phrase_id))
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Track structure analysis warning for {target_file}: {e}")
|
||||
duration = 2.5
|
||||
finally:
|
||||
if phrase_id and 'conn' in locals() and conn:
|
||||
conn.close()
|
||||
|
||||
return duration
|
||||
|
||||
# =====================================================================
|
||||
# 🗄️ TAB 1: TRANSLATION-CENTRIC PHRASE SANDBOX (CRUD)
|
||||
# =====================================================================
|
||||
|
|
@ -269,11 +167,9 @@ class SpanishTrainerApp(QMainWindow):
|
|||
|
||||
self.input_text_es = QTextEdit()
|
||||
self.input_text_es.setMaximumHeight(75)
|
||||
self.input_text_es.textChanged.connect(self.clear_id_if_new_entry)
|
||||
|
||||
self.input_text_en = QTextEdit()
|
||||
self.input_text_en.setMaximumHeight(75)
|
||||
self.input_text_en.textChanged.connect(self.clear_id_if_new_entry)
|
||||
|
||||
self.combo_type = QComboBox()
|
||||
self.combo_type.addItems(["phrase", "sentence", "noun", "verb", "adjective"])
|
||||
|
|
@ -285,7 +181,7 @@ class SpanishTrainerApp(QMainWindow):
|
|||
self.input_tags.setPlaceholderText("e.g., irregular_er boots_verb")
|
||||
|
||||
self.input_deck_tag = QLineEdit()
|
||||
self.input_deck_tag.setPlaceholderText("Overrides System Default Workspace Deck")
|
||||
self.input_deck_tag.setPlaceholderText("Anki Sub-deck Hierarchy")
|
||||
|
||||
button_qss = """
|
||||
QPushButton {
|
||||
|
|
@ -362,7 +258,7 @@ class SpanishTrainerApp(QMainWindow):
|
|||
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 (Optional):</b>", self.input_deck_tag)
|
||||
form_layout.addRow("<b>Target Deck Scope:</b>", self.input_deck_tag)
|
||||
|
||||
crud_buttons = QHBoxLayout()
|
||||
self.btn_save = QPushButton("➕ Create Pair")
|
||||
|
|
@ -457,7 +353,7 @@ class SpanishTrainerApp(QMainWindow):
|
|||
|
||||
action_buttons = QHBoxLayout()
|
||||
self.btn_play_voice = QPushButton("🗣️ Play Voice Track")
|
||||
self.btn_flip_card = QPushButton("👁️ Reveal Translation")
|
||||
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)
|
||||
|
|
@ -502,6 +398,7 @@ class SpanishTrainerApp(QMainWindow):
|
|||
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)
|
||||
|
|
@ -510,6 +407,7 @@ class SpanishTrainerApp(QMainWindow):
|
|||
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)
|
||||
|
|
@ -518,53 +416,17 @@ class SpanishTrainerApp(QMainWindow):
|
|||
video_layout.addWidget(self.line_video_dir)
|
||||
video_layout.addWidget(btn_browse_video)
|
||||
|
||||
# New Workspace Target Settings Directive Entry
|
||||
self.line_default_deck = QLineEdit(self.default_deck_name)
|
||||
self.line_default_deck.setPlaceholderText("e.g., Spanish::Aula_Plus_1")
|
||||
self.line_default_deck.textChanged.connect(lambda v: self.save_setting_to_db("default_deck_name", v.strip()))
|
||||
|
||||
self.combo_first_lang = QComboBox()
|
||||
self.combo_first_lang.addItems(["English First (en -> es)", "Spanish First (es -> en)"])
|
||||
self.combo_first_lang.setCurrentText(self.video_first_lang)
|
||||
self.combo_first_lang.currentTextChanged.connect(lambda v: self.save_setting_to_db("video_first_language", v))
|
||||
|
||||
self.spin_video_repeats = QLineEdit(self.video_repeats_count)
|
||||
self.spin_video_repeats.setFixedWidth(60)
|
||||
self.spin_video_repeats.textChanged.connect(lambda v: self.save_setting_to_db("video_repeats_count", v))
|
||||
|
||||
self.spin_pause_duration = QLineEdit(self.video_pause_duration)
|
||||
self.spin_pause_duration.setFixedWidth(60)
|
||||
self.spin_pause_duration.textChanged.connect(lambda v: self.save_setting_to_db("video_pause_duration", v))
|
||||
|
||||
form_layout.addRow("<b>Anki Deck Export Destination:</b>", anki_layout)
|
||||
form_layout.addRow("<b>Video Assembly Output Target:</b>", video_layout)
|
||||
form_layout.addRow("<b>Default Target Deck String:</b>", self.line_default_deck)
|
||||
form_layout.addRow("<b>Introductory Anchor Audio Language:</b>", self.combo_first_lang)
|
||||
form_layout.addRow("<b>Target Translation Loop Multiplier (Repeats):</b>", self.spin_video_repeats)
|
||||
form_layout.addRow("<b>User Recall Repetition Frame Intermission (Seconds):</b>", self.spin_pause_duration)
|
||||
|
||||
self.btn_sync_cache = QPushButton("⚡ Populate Audio & Timings Cache")
|
||||
self.btn_sync_cache.setStyleSheet("""
|
||||
QPushButton {
|
||||
font-weight: bold;
|
||||
background-color: #e67e22;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
QPushButton:hover { background-color: #d35400; }
|
||||
""")
|
||||
self.btn_sync_cache.clicked.connect(self.handle_bulk_populate_audio_cache)
|
||||
|
||||
layout.addWidget(QLabel("<h2>Application Preferences & Workspace Routing</h2>"))
|
||||
layout.addWidget(settings_frame)
|
||||
layout.addWidget(self.btn_sync_cache)
|
||||
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
|
||||
|
|
@ -572,402 +434,13 @@ class SpanishTrainerApp(QMainWindow):
|
|||
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)
|
||||
|
||||
def handle_bulk_populate_audio_cache(self):
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT p1.id, p1.text, p2.id, p2.text
|
||||
FROM translations t
|
||||
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
||||
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
||||
""")
|
||||
records = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
if not records:
|
||||
QMessageBox.information(self, "Cache Synchronizer", "No valid translation pairs exist inside the database to process.")
|
||||
return
|
||||
|
||||
print(f"⚡ Processing structural cache updates for {len(records)} node linkages...")
|
||||
for row in records:
|
||||
es_id, es_text, en_id, en_text = row[0], row[1].strip(), row[2], row[3].strip()
|
||||
self.get_or_generate_audio_duration(en_id, en_text, "en")
|
||||
self.get_or_generate_audio_duration(es_id, es_text, "es")
|
||||
|
||||
QMessageBox.information(self, "Cache Processing Complete", "All missing speech segments successfully written. Timings cached safely.")
|
||||
|
||||
# =====================================================================
|
||||
# 💡 FLASHCARD OPERATIONS LOGIC COUPLING
|
||||
# =====================================================================
|
||||
def handle_live_speed_change(self):
|
||||
val = self.slider_review_speed.value()
|
||||
self.lbl_review_speed.setText(f"{val / 100:.2f}x")
|
||||
|
||||
def load_flashcard_by_id(self, translation_id):
|
||||
"""Loads a translation node into memory and targets local text widgets without notes clutter."""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT t.translation_id, p1.text, p2.text, p1.source_context, t.tags, t.notes
|
||||
FROM translations t
|
||||
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
||||
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
||||
WHERE t.translation_id = ?
|
||||
""", (translation_id,))
|
||||
record = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if record:
|
||||
self.current_flashcard_id = record[0]
|
||||
self.current_active_es_text = str(record[1])
|
||||
self.current_active_en_text = str(record[2])
|
||||
self.current_card_is_flipped = False
|
||||
|
||||
self.lbl_card_text.setText(self.current_active_en_text)
|
||||
self.btn_flip_card.setText("👁️ Reveal Translation")
|
||||
|
||||
meta_str = f"Link ID: {record[0]} | Context: {record[3] or 'N/A'}"
|
||||
if record[4]:
|
||||
meta_str += f" | Tags: {record[4]}"
|
||||
self.lbl_card_meta.setText(meta_str)
|
||||
|
||||
self.handle_play_voice()
|
||||
|
||||
def handle_flip_card(self):
|
||||
if not self.current_flashcard_id:
|
||||
return
|
||||
|
||||
if not self.current_card_is_flipped:
|
||||
self.lbl_card_text.setText(self.current_active_es_text)
|
||||
self.btn_flip_card.setText("👁️ Return to Prompt")
|
||||
self.current_card_is_flipped = True
|
||||
else:
|
||||
self.lbl_card_text.setText(self.current_active_en_text)
|
||||
self.btn_flip_card.setText("👁️ Reveal Translation")
|
||||
self.current_card_is_flipped = False
|
||||
|
||||
def handle_play_voice(self):
|
||||
if not self.current_flashcard_id:
|
||||
return
|
||||
|
||||
text_target = self.current_active_es_text if self.current_card_is_flipped else self.current_active_en_text
|
||||
lang_target = "es" if self.current_card_is_flipped else "en"
|
||||
speed_target = self.lbl_review_speed.text()
|
||||
|
||||
self.execute_playback(text_target, lang_target, speed_target)
|
||||
|
||||
def handle_load_next_card(self):
|
||||
if not self.flashcard_ids_pool:
|
||||
QMessageBox.information(self, "Pool Empty", "No flashcards found in the matrix matching current criteria filters.")
|
||||
return
|
||||
|
||||
next_tx_id = random.choice(self.flashcard_ids_pool)
|
||||
|
||||
for row in range(self.review_table.rowCount()):
|
||||
if int(self.review_table.item(row, 0).text()) == next_tx_id:
|
||||
self.review_table.setCurrentCell(row, 0)
|
||||
break
|
||||
|
||||
self.load_flashcard_by_id(next_tx_id)
|
||||
|
||||
# =====================================================================
|
||||
# 📦 GENANKI EXPORT ENGINE (RESTRUCTURED PURE TRANSLATION FLOW)
|
||||
# =====================================================================
|
||||
def handle_export_anki_deck(self):
|
||||
"""
|
||||
Gathers selected records from the matching criteria pool view, builds
|
||||
a dual card template layout mapping English->Spanish (Card 1) and Spanish->English
|
||||
(Card 2) forward-reverse pairs cleanly. Explicitly maps 4 fields: EnglishText,
|
||||
EnglishAudio, SpanishText, SpanishAudio. Context and notes are fully removed.
|
||||
"""
|
||||
# Architectural Fallback: If pool is empty, run an internal update pass first to grab current matrix records
|
||||
if not self.flashcard_ids_pool:
|
||||
self.refresh_review_table()
|
||||
|
||||
targets = self.flashcard_ids_pool
|
||||
if not targets:
|
||||
QMessageBox.warning(self, "Export Aborted", "The active flashcard pool filter is completely empty. Nothing to export.")
|
||||
return
|
||||
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
placeholders = ",".join("?" for _ in targets)
|
||||
cursor.execute(f"""
|
||||
SELECT t.translation_id, p1.text, p2.text, p1.source_context, t.tags, t.notes, t.deck_name
|
||||
FROM translations t
|
||||
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
||||
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
||||
WHERE t.translation_id IN ({placeholders})
|
||||
""", targets)
|
||||
records = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
if not records:
|
||||
QMessageBox.information(self, "Export Processing", "No structured database entries matched your criteria indices parameters.")
|
||||
return
|
||||
|
||||
# Unique Identification Anchor Codes for Anki Database Integrity
|
||||
MODEL_ID = 1684321095
|
||||
DECK_ID = 2026062011
|
||||
|
||||
# New Model structure aligning English Text/Audio with Spanish Text/Audio sequences
|
||||
spanish_model = genanki.Model(
|
||||
MODEL_ID,
|
||||
'Castilian Learning Model (Pure Text & Audio Alignment)',
|
||||
fields=[
|
||||
{'name': 'EnglishText'},
|
||||
{'name': 'EnglishAudio'},
|
||||
{'name': 'SpanishText'},
|
||||
{'name': 'SpanishAudio'}
|
||||
],
|
||||
templates=[
|
||||
{
|
||||
'name': 'Card 1: English -> Spanish',
|
||||
'qfmt': (
|
||||
'<div style="font-size: 13px; color: #2980b9; font-weight: bold; margin-bottom: 12px; letter-spacing: 1px;">🇬🇧 ENGLISH COMPREHENSION</div>'
|
||||
'<div style="font-size: 25px; text-align: center; color: #2c3e50; font-family: Arial;">{{EnglishText}}</div>'
|
||||
'<br><div style="text-align:center;">{{EnglishAudio}}</div>'
|
||||
),
|
||||
'afmt': (
|
||||
'{{FrontSide}}<hr id="answer">'
|
||||
'<div style="font-size: 13px; color: #e74c3c; font-weight: bold; margin-bottom: 12px; letter-spacing: 1px;">🇪🇸 SPANISH PRODUCTION</div>'
|
||||
'<div style="font-size: 25px; text-align: center; color: #16a085; font-family: Arial; font-weight: bold;">{{SpanishText}}</div>'
|
||||
'<br><div style="text-align:center;">{{SpanishAudio}}</div>'
|
||||
),
|
||||
},
|
||||
{
|
||||
'name': 'Card 2: Spanish -> English',
|
||||
'qfmt': (
|
||||
'<div style="font-size: 13px; color: #e74c3c; font-weight: bold; margin-bottom: 12px; letter-spacing: 1px;">🇪🇸 SPANISH PRODUCTION</div>'
|
||||
'<div style="font-size: 25px; text-align: center; color: #2c3e50; font-family: Arial;">{{SpanishText}}</div>'
|
||||
'<br><div style="text-align:center;">{{SpanishAudio}}</div>'
|
||||
),
|
||||
'afmt': (
|
||||
'{{FrontSide}}<hr id="answer">'
|
||||
'<div style="font-size: 13px; color: #2980b9; font-weight: bold; margin-bottom: 12px; letter-spacing: 1px;">🇬🇧 ENGLISH COMPREHENSION</div>'
|
||||
'<div style="font-size: 25px; text-align: center; color: #16a085; font-family: Arial; font-weight: bold;">{{EnglishText}}</div>'
|
||||
'<br><div style="text-align:center;">{{EnglishAudio}}</div>'
|
||||
),
|
||||
},
|
||||
],
|
||||
css='.card { font-family: arial; font-size: 20px; text-align: center; background-color: #fafafa; padding: 25px; border-radius: 8px; }'
|
||||
)
|
||||
|
||||
# --- RESTRUCTURED STRINGS SAFEGUARD BLOCK ---
|
||||
# Safeguard checks to normalize index 6 data entry and prevent generation string crashes
|
||||
raw_deck_entry = records[0][6]
|
||||
if raw_deck_entry and str(raw_deck_entry).strip():
|
||||
deck_name_fallback = str(raw_deck_entry).strip()
|
||||
else:
|
||||
deck_name_fallback = self.default_deck_name if self.default_deck_name else "Trainer"
|
||||
|
||||
# Explicitly ensure the Anki namespace structural hierarchy sequence is intact
|
||||
full_deck_string = f"Spanish::{deck_name_fallback}" if "Spanish::" not in deck_name_fallback else deck_name_fallback
|
||||
|
||||
anki_deck = genanki.Deck(DECK_ID, full_deck_string)
|
||||
media_files_bundle = []
|
||||
|
||||
print(f"📦 Assembling Anki audio package for {len(records)} notes under deck: {full_deck_string}...")
|
||||
|
||||
for row in records:
|
||||
tx_id, es_text, en_text, context, tags, notes, deck_group = row
|
||||
es_clean = es_text.strip()
|
||||
en_clean = en_text.strip()
|
||||
|
||||
# --- Spanish Media Track Setup ---
|
||||
safe_es_name = "".join([c for c in es_clean if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
||||
if not safe_es_name:
|
||||
safe_es_name = hashlib.sha256(es_clean.encode('utf-8')).hexdigest()[:16]
|
||||
audio_es_filename = f"{safe_es_name}_es_female.mp3"
|
||||
full_es_path = f"media/{audio_es_filename}"
|
||||
|
||||
if not os.path.exists(full_es_path):
|
||||
self.get_or_generate_audio_duration(None, es_clean, "es")
|
||||
if os.path.exists(full_es_path):
|
||||
media_files_bundle.append(full_es_path)
|
||||
es_audio_tag = f"[sound:{audio_es_filename}]"
|
||||
else:
|
||||
es_audio_tag = ""
|
||||
|
||||
# --- English Media Track Setup ---
|
||||
safe_en_name = "".join([c for c in en_clean if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
||||
if not safe_en_name:
|
||||
safe_en_name = hashlib.sha256(en_clean.encode('utf-8')).hexdigest()[:16]
|
||||
audio_en_filename = f"{safe_en_name}_en_female.mp3"
|
||||
full_en_path = f"media/{audio_en_filename}"
|
||||
|
||||
if not os.path.exists(full_en_path):
|
||||
self.get_or_generate_audio_duration(None, en_clean, "en")
|
||||
if os.path.exists(full_en_path):
|
||||
media_files_bundle.append(full_en_path)
|
||||
en_audio_tag = f"[sound:{audio_en_filename}]"
|
||||
else:
|
||||
en_audio_tag = ""
|
||||
|
||||
# Meta and Category Tag Construction
|
||||
tag_list = str(tags).split() if tags else []
|
||||
if context:
|
||||
tag_list.append(str(context).replace(" ", "_").replace(".", "_"))
|
||||
|
||||
anki_note = genanki.Note(
|
||||
model=spanish_model,
|
||||
fields=[
|
||||
en_text,
|
||||
en_audio_tag,
|
||||
es_text,
|
||||
es_audio_tag
|
||||
],
|
||||
tags=tag_list
|
||||
)
|
||||
anki_deck.add_note(anki_note)
|
||||
|
||||
# Output file name normalization optimization to sweep unmapped character sets
|
||||
safe_file_name = deck_name_fallback.replace('::', '_').replace('/', '_').strip()
|
||||
export_output_path = os.path.join(self.anki_export_dir, f"{safe_file_name}.apkg")
|
||||
|
||||
try:
|
||||
package = genanki.Package(anki_deck)
|
||||
package.media_files = list(set(media_files_bundle))
|
||||
package.write_to_file(export_output_path)
|
||||
|
||||
print(f"✅ Success! Balanced text-audio cards exported cleanly: {export_output_path}")
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Anki Package Compiled",
|
||||
f"Successfully compiled {len(records)} balanced text-audio translation flashcard nodes.\n\nDestination:\n{export_output_path}"
|
||||
)
|
||||
except Exception as export_err:
|
||||
print(f"❌ Genanki Write Failure Exception Error: {export_err}")
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Export Failure Error",
|
||||
f"The packaging sub-engine failed to write file to disk:\n{export_err}"
|
||||
)
|
||||
|
||||
def handle_export_video_assets(self):
|
||||
pass
|
||||
|
||||
# =====================================================================
|
||||
# ➕ CRUD ENGINE ATOMIC OPERATIONS LOGIC
|
||||
# =====================================================================
|
||||
def crud_create_pair(self):
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, word_type, source_context) VALUES (?, 'es', ?, ?)
|
||||
""", (self.input_text_es.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip()))
|
||||
es_id = cursor.lastrowid
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, word_type, source_context) VALUES (?, 'en', ?, ?)
|
||||
""", (self.input_text_en.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip()))
|
||||
en_id = cursor.lastrowid
|
||||
|
||||
# Pulls from self.input_deck_tag if typed, cleanly defaults to global persistent variable self.default_deck_name
|
||||
target_deck = self.input_deck_tag.text().strip() or self.default_deck_name
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO translations (source_phrase_id, target_phrase_id, deck_name, notes, tags) VALUES (?, ?, ?, ?, ?)
|
||||
""", (es_id, en_id, target_deck, self.input_grammar_note.toPlainText().strip(), self.input_tags.text().strip()))
|
||||
tx_id = cursor.lastrowid
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
self.input_tx_id.setText(str(tx_id))
|
||||
self.current_sandbox_es_id = es_id
|
||||
self.current_sandbox_en_id = en_id
|
||||
|
||||
self.refresh_crud_table()
|
||||
self.refresh_review_table()
|
||||
QMessageBox.information(self, "Success", f"Isolated phrase pairs created and bound to Translation ID {tx_id}.")
|
||||
|
||||
def crud_update_pair(self):
|
||||
tx_id_str = self.input_tx_id.text().strip()
|
||||
if not tx_id_str:
|
||||
QMessageBox.warning(self, "Update Target Missing", "No Translation Link ID found. Select an existing record node or create a fresh link pair first.")
|
||||
return
|
||||
|
||||
if self.current_sandbox_es_id is None or self.current_sandbox_en_id is None:
|
||||
QMessageBox.warning(self, "Phrase Nodes Untracked", "Underlying unique identifiers for individual language components are missing. Reselect the row from the left panel matrix grid.")
|
||||
return
|
||||
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute("""
|
||||
UPDATE phrases
|
||||
SET text = ?, word_type = ?, source_context = ?, duration = NULL
|
||||
WHERE id = ?
|
||||
""", (self.input_text_es.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip(), self.current_sandbox_es_id))
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE phrases
|
||||
SET text = ?, word_type = ?, source_context = ?, duration = NULL
|
||||
WHERE id = ?
|
||||
""", (self.input_text_en.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip(), self.current_sandbox_en_id))
|
||||
|
||||
# Updated field configuration using system preferences cache fallback assignment
|
||||
target_deck = self.input_deck_tag.text().strip() or self.default_deck_name
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE translations
|
||||
SET deck_name = ?, notes = ?, tags = ?
|
||||
WHERE translation_id = ?
|
||||
""", (target_deck, self.input_grammar_note.toPlainText().strip(), self.input_tags.text().strip(), int(tx_id_str)))
|
||||
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Database Error", f"Failed to execute field modifications inside SQL engine: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
self.refresh_crud_table()
|
||||
self.refresh_review_table()
|
||||
QMessageBox.information(self, "Success", f"Node structural fields updated successfully. Translation Link ID {tx_id_str} remains active.")
|
||||
|
||||
def crud_delete_pair(self):
|
||||
pass
|
||||
|
||||
def execute_playback(self, text_str, lang, speed_text):
|
||||
txt = text_str.strip()
|
||||
if not txt:
|
||||
return
|
||||
|
||||
self.get_or_generate_audio_duration(None, txt, lang)
|
||||
|
||||
safe_name = "".join([c for c in txt if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
||||
if not safe_name:
|
||||
safe_name = hashlib.sha256(txt.encode('utf-8')).hexdigest()[:16]
|
||||
|
||||
target_file = f"media/{safe_name}_{lang}_female.mp3"
|
||||
|
||||
if os.path.exists(target_file):
|
||||
try:
|
||||
multiplier = float(speed_text.replace("x", ""))
|
||||
except ValueError:
|
||||
multiplier = 1.0
|
||||
|
||||
self.media_player.stop()
|
||||
self.media_player.setSource(QUrl.fromLocalFile(os.path.abspath(target_file)))
|
||||
self.media_player.setLoops(1)
|
||||
self.media_player.setPlaybackRate(multiplier)
|
||||
self.media_player.play()
|
||||
|
||||
def handle_sandbox_play_es(self):
|
||||
self.execute_playback(self.input_text_es.toPlainText(), "es", self.combo_speed_es.currentText())
|
||||
|
||||
def handle_sandbox_play_en(self):
|
||||
self.execute_playback(self.input_text_en.toPlainText(), "en", self.combo_speed_en.currentText())
|
||||
|
||||
# =====================================================================
|
||||
# ⚡ DATA MATRIX CONTROL & SYNCHRONIZATION VIEWS
|
||||
# =====================================================================
|
||||
|
|
@ -1016,7 +489,7 @@ class SpanishTrainerApp(QMainWindow):
|
|||
tag_filter = self.review_tag_filter.text().strip()
|
||||
|
||||
query = """
|
||||
SELECT t.translation_id, p1.text, p1.source_context, t.tags
|
||||
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'
|
||||
|
|
@ -1040,7 +513,7 @@ class SpanishTrainerApp(QMainWindow):
|
|||
|
||||
for row_idx, row_data in enumerate(rows):
|
||||
self.review_table.insertRow(row_idx)
|
||||
self.flashcard_ids_pool.append(row_data[0])
|
||||
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 "")))
|
||||
|
|
@ -1059,7 +532,7 @@ class SpanishTrainerApp(QMainWindow):
|
|||
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, p1.id, p2.id
|
||||
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
|
||||
|
|
@ -1075,19 +548,16 @@ class SpanishTrainerApp(QMainWindow):
|
|||
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 "")
|
||||
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 "")
|
||||
self.current_sandbox_es_id = record[8]
|
||||
self.current_sandbox_en_id = record[9]
|
||||
|
||||
def handle_review_table_select(self):
|
||||
selected_ranges = self.review_table.selectedRanges()
|
||||
if not selected_ranges:
|
||||
return
|
||||
row = selected_ranges[0].topRow()
|
||||
if row < len(self.flashcard_ids_pool):
|
||||
translation_id = self.flashcard_ids_pool[row]
|
||||
self.load_flashcard_by_id(translation_id)
|
||||
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()
|
||||
|
|
@ -1095,12 +565,252 @@ class SpanishTrainerApp(QMainWindow):
|
|||
if 0 <= next_row < self.translation_table.rowCount():
|
||||
self.translation_table.setCurrentCell(next_row, 0)
|
||||
|
||||
def clear_id_if_new_entry(self):
|
||||
if self.input_tx_id.text() and not (self.input_text_es.hasFocus() or self.input_text_en.hasFocus()):
|
||||
pass
|
||||
# =====================================================================
|
||||
# ➕ 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__":
|
||||
app = QApplication(sys.argv)
|
||||
window = SpanishTrainerApp()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
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)
|
||||
0
spanish__trainer.db
Normal file
0
spanish__trainer.db
Normal file
Binary file not shown.
Loading…
Reference in a new issue