added sitting tab

This commit is contained in:
stephen 2026-06-19 15:09:05 +10:00
parent ae68ffd199
commit f03115a06a
4 changed files with 6352 additions and 5529 deletions

View file

@ -37,6 +37,11 @@
- [how to backup the sqlite3 database](#how-to-backup-the-sqlite3-database) - [how to backup the sqlite3 database](#how-to-backup-the-sqlite3-database)
- [how ro alter a table within the data base](#how-ro-alter-a-table-within-the-data-base) - [how ro alter a table within the data base](#how-ro-alter-a-table-within-the-data-base)
- [to examine the PRAGMA table](#to-examine-the-pragma-table) - [to examine the PRAGMA table](#to-examine-the-pragma-table)
- [1. The Cleanest Output: .schema](#1-the-cleanest-output-schema)
- [sqlite3 schema](#sqlite3-schema)
- [sqlite3 fullschema](#sqlite3-fullschema)
- [Pro-Tip: Running it Interactively](#pro-tip-running-it-interactively)
- [how to add a new table via command line](#how-to-add-a-new-table-via-command-line)
# 1. spanish-voice-trainer # 1. spanish-voice-trainer
# 2. Project Summary: # 2. Project Summary:
@ -470,4 +475,47 @@ sqlite3 spanish_trainer.db "ALTER TABLE translations ADD COLUMN tags TEXT DEFAUL
sqlite3 spanish_trainer.db "PRAGMA table_info(translations);" sqlite3 spanish_trainer.db "PRAGMA table_info(translations);"
``` ```
## 1. The Cleanest Output: .schema
The most precise command is the dot-command .schema. You can pass it directly into the CLI call:
## sqlite3 schema
```zsh
sqlite3 spanish_trainer.db ".schema"
```
Why this is exactly what you need:
It prints out the exact SQL CREATE TABLE statements used to build your entire database structure.
It automatically includes any constraints (like PRIMARY KEY, FOREIGN KEY, or UNIQUE).
It shows all other structural items, such as your indexes and triggers, giving you a complete blueprint of the file.
2. The Comprehensive Alternative: .fullschema
If you ever start implementing complex custom database views or virtual tables down the road, you can use:
## sqlite3 fullschema
```zsh
sqlite3 spanish_trainer.db ".fullschema"
```
This does everything .schema does, but it also appends statistical metadata and structural configuration variables used to optimize the query planner.
## Pro-Tip: Running it Interactively
If you are already inside an active SQLite session using your terminal, you don't need to specify the database name or use quotation marks. You can just type the dot-command at the prompt:
SQL
sqlite> .schema
If you want a more compact table checklist just to see what names are present before inspecting their structures, you can use:
SQL
sqlite> .tables
## how to add a new table via command line
```zsh
sqlite3 spanish_trainer.db "CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT);"
```

File diff suppressed because it is too large Load diff

127
main.py
View file

@ -4,7 +4,7 @@ import os
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout, QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QLineEdit, QComboBox, QHBoxLayout, QLabel, QPushButton, QLineEdit, QComboBox,
QTableWidget, QTableWidgetItem, QSlider, QFormLayout, QTextEdit, QFrame, QMessageBox QTableWidget, QTableWidgetItem, QSlider, QFormLayout, QTextEdit, QFrame, QMessageBox, QFileDialog
) )
from PyQt6.QtCore import Qt, QUrl from PyQt6.QtCore import Qt, QUrl
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
@ -24,6 +24,9 @@ class SpanishTrainerApp(QMainWindow):
# 1. Initialize schema structures and check ingestion status # 1. Initialize schema structures and check ingestion status
self.ensure_database_populated() self.ensure_database_populated()
# Load system persistent settings from DB
self.load_system_settings()
# Audio Player Architecture Setup # Audio Player Architecture Setup
self.media_player = QMediaPlayer() self.media_player = QMediaPlayer()
self.audio_output = QAudioOutput() self.audio_output = QAudioOutput()
@ -38,8 +41,9 @@ class SpanishTrainerApp(QMainWindow):
self.init_phrase_sandbox_tab() self.init_phrase_sandbox_tab()
self.init_flashcard_reviewer_tab() self.init_flashcard_reviewer_tab()
self.init_settings_tab() # Mount the new settings panel
# 2. Populate both table grids on initialization # 2. Populate table grids on initialization
self.refresh_crud_table() self.refresh_crud_table()
self.refresh_review_table() self.refresh_review_table()
@ -51,6 +55,9 @@ class SpanishTrainerApp(QMainWindow):
conn = get_connection() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
# Ensure the settings table exists alongside legacy core tables
cursor.execute("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT);")
try: try:
cursor.execute("SELECT COUNT(*) FROM translations") cursor.execute("SELECT COUNT(*) FROM translations")
count = cursor.fetchone()[0] count = cursor.fetchone()[0]
@ -75,6 +82,38 @@ class SpanishTrainerApp(QMainWindow):
else: else:
print(f"❌ Error: Source document '{pdf_file}' is missing from the directory root.") print(f"❌ Error: Source document '{pdf_file}' is missing from the directory root.")
def load_system_settings(self):
"""Loads persistent path directories from the key-value settings table."""
self.anki_export_dir = os.getcwd()
self.video_export_dir = os.getcwd()
conn = get_connection()
cursor = conn.cursor()
try:
cursor.execute("SELECT key, value FROM settings")
rows = cursor.fetchall()
for row in rows:
if row[0] == "anki_export_directory":
self.anki_export_dir = row[1]
elif row[0] == "video_export_directory":
self.video_export_dir = row[1]
except Exception as e:
print(f"⚠️ Failed to read application settings from database: {e}")
finally:
conn.close()
def save_setting_to_db(self, key, value):
"""Updates or inserts a specific system runtime variable into the database."""
conn = get_connection()
cursor = conn.cursor()
try:
cursor.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", (key, value))
conn.commit()
except Exception as e:
print(f"❌ Critical: Failed to save setting '{key}': {e}")
finally:
conn.close()
# ===================================================================== # =====================================================================
# 🗄️ TAB 1: TRANSLATION-CENTRIC PHRASE SANDBOX (CRUD) # 🗄️ TAB 1: TRANSLATION-CENTRIC PHRASE SANDBOX (CRUD)
# ===================================================================== # =====================================================================
@ -245,13 +284,12 @@ class SpanishTrainerApp(QMainWindow):
self.tabs.addTab(tab, "🗄️ Phrase Sandbox (CRUD)") self.tabs.addTab(tab, "🗄️ Phrase Sandbox (CRUD)")
# ===================================================================== # =====================================================================
# 🃏 TAB 2: FLASHCARD STUDY MODULE (UPGRADED TWIN FRAME WINDOW) # 🃏 TAB 2: FLASHCARD STUDY MODULE
# ===================================================================== # =====================================================================
def init_flashcard_reviewer_tab(self): def init_flashcard_reviewer_tab(self):
tab = QWidget() tab = QWidget()
layout = QHBoxLayout(tab) layout = QHBoxLayout(tab)
# --- LEFT SIDE PANEL: Symmetrical Review Filters & Substack Table ---
left_panel = QVBoxLayout() left_panel = QVBoxLayout()
filter_layout = QHBoxLayout() filter_layout = QHBoxLayout()
@ -277,10 +315,8 @@ class SpanishTrainerApp(QMainWindow):
layout.addLayout(left_panel, stretch=4) layout.addLayout(left_panel, stretch=4)
# --- RIGHT SIDE PANEL: Workspace Review Controls & Active Layout Flashcard Canvas ---
right_panel = QVBoxLayout() right_panel = QVBoxLayout()
# Large Display Flashcard Frame Window Canvas (Top Half Block)
card_frame = QFrame() card_frame = QFrame()
card_frame.setStyleSheet("background-color: #ffffff; border: 2px solid #bdc3c7; border-radius: 12px;") card_frame.setStyleSheet("background-color: #ffffff; border: 2px solid #bdc3c7; border-radius: 12px;")
card_layout = QVBoxLayout(card_frame) card_layout = QVBoxLayout(card_frame)
@ -303,7 +339,6 @@ class SpanishTrainerApp(QMainWindow):
card_layout.addStretch() card_layout.addStretch()
right_panel.addWidget(card_frame, stretch=4) right_panel.addWidget(card_frame, stretch=4)
# Middle Operational Playback Stack Controls
playback_layout = QHBoxLayout() playback_layout = QHBoxLayout()
playback_layout.addWidget(QLabel("🔊 Voice Speed:")) playback_layout.addWidget(QLabel("🔊 Voice Speed:"))
self.slider_review_speed = QSlider(Qt.Orientation.Horizontal) self.slider_review_speed = QSlider(Qt.Orientation.Horizontal)
@ -329,7 +364,6 @@ class SpanishTrainerApp(QMainWindow):
right_panel.addSpacing(15) right_panel.addSpacing(15)
# Bottom Utility Navigation & Deployment Array
bottom_utility_layout = QHBoxLayout() bottom_utility_layout = QHBoxLayout()
self.btn_export_anki = QPushButton("📦 Export Anki Deck") self.btn_export_anki = QPushButton("📦 Export Anki Deck")
self.btn_export_video = QPushButton("🎬 Export Video") self.btn_export_video = QPushButton("🎬 Export Video")
@ -339,7 +373,6 @@ class SpanishTrainerApp(QMainWindow):
self.btn_export_video.clicked.connect(self.handle_export_video_assets) self.btn_export_video.clicked.connect(self.handle_export_video_assets)
self.btn_load_next.clicked.connect(self.handle_load_next_card) 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; }" utility_qss = "QPushButton { font-weight: bold; background-color: #eaf2f8; padding: 6px; border-radius: 4px; }"
self.btn_export_anki.setStyleSheet(utility_qss) self.btn_export_anki.setStyleSheet(utility_qss)
self.btn_export_video.setStyleSheet(utility_qss) self.btn_export_video.setStyleSheet(utility_qss)
@ -354,11 +387,64 @@ class SpanishTrainerApp(QMainWindow):
layout.addLayout(right_panel, stretch=3) layout.addLayout(right_panel, stretch=3)
self.tabs.addTab(tab, "🃏 Flashcard Review") self.tabs.addTab(tab, "🃏 Flashcard Review")
# =====================================================================
# ⚙️ TAB 3: SYSTEM HARDWARE & EXPORT SETTINGS
# =====================================================================
def init_settings_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
settings_frame = QFrame()
settings_frame.setFrameShape(QFrame.Shape.StyledPanel)
form_layout = QFormLayout(settings_frame)
# Anki Export Path Form Group
anki_layout = QHBoxLayout()
self.line_anki_dir = QLineEdit(self.anki_export_dir)
self.line_anki_dir.setReadOnly(True)
btn_browse_anki = QPushButton("Browse 📂")
btn_browse_anki.clicked.connect(self.handle_browse_anki_directory)
anki_layout.addWidget(self.line_anki_dir)
anki_layout.addWidget(btn_browse_anki)
# Video Export Path Form Group
video_layout = QHBoxLayout()
self.line_video_dir = QLineEdit(self.video_export_dir)
self.line_video_dir.setReadOnly(True)
btn_browse_video = QPushButton("Browse 📂")
btn_browse_video.clicked.connect(self.handle_browse_video_directory)
video_layout.addWidget(self.line_video_dir)
video_layout.addWidget(btn_browse_video)
form_layout.addRow("<b>Anki Deck Export Destination:</b>", anki_layout)
form_layout.addRow("<b>Video Assembly Output Target:</b>", video_layout)
layout.addWidget(QLabel("<h2>Application Preferences & Workspace Routing</h2>"))
layout.addWidget(settings_frame)
layout.addStretch()
self.tabs.addTab(tab, "⚙️ Settings")
def handle_browse_anki_directory(self):
"""Triggers a file browser directory selection for native Anki card compiles."""
directory = QFileDialog.getExistingDirectory(self, "Select Anki Export Folder", self.anki_export_dir)
if directory:
self.anki_export_dir = directory
self.line_anki_dir.setText(directory)
self.save_setting_to_db("anki_export_directory", directory)
def handle_browse_video_directory(self):
"""Triggers a file browser directory selection for video timeline outputs."""
directory = QFileDialog.getExistingDirectory(self, "Select Video Export Folder", self.video_export_dir)
if directory:
self.video_export_dir = directory
self.line_video_dir.setText(directory)
self.save_setting_to_db("video_export_directory", directory)
# ===================================================================== # =====================================================================
# ⚡ DATA MATRIX CONTROL & SYNCHRONIZATION VIEWS # ⚡ DATA MATRIX CONTROL & SYNCHRONIZATION VIEWS
# ===================================================================== # =====================================================================
def refresh_crud_table(self): def refresh_crud_table(self):
"""Pulls unified translation nodes into pairs while enforcing context text matches."""
conn = get_connection() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
@ -396,7 +482,6 @@ class SpanishTrainerApp(QMainWindow):
self.translation_table.setItem(row_idx, col_idx, QTableWidgetItem(str(val if val is not None else ""))) self.translation_table.setItem(row_idx, col_idx, QTableWidgetItem(str(val if val is not None else "")))
def refresh_review_table(self): def refresh_review_table(self):
"""Pulls and displays a targeted subset stack matching specific context or tag filters."""
conn = get_connection() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
@ -428,7 +513,7 @@ class SpanishTrainerApp(QMainWindow):
for row_idx, row_data in enumerate(rows): for row_idx, row_data in enumerate(rows):
self.review_table.insertRow(row_idx) self.review_table.insertRow(row_idx)
self.flashcard_ids_pool.append(row_data[4]) # Track active selection text key reference IDs self.flashcard_ids_pool.append(row_data[4])
for col_idx in range(4): for col_idx in range(4):
val = row_data[col_idx] val = row_data[col_idx]
self.review_table.setItem(row_idx, col_idx, QTableWidgetItem(str(val if val is not None else ""))) self.review_table.setItem(row_idx, col_idx, QTableWidgetItem(str(val if val is not None else "")))
@ -471,7 +556,6 @@ class SpanishTrainerApp(QMainWindow):
if not selected_ranges: if not selected_ranges:
return return
row = selected_ranges[0].topRow() row = selected_ranges[0].topRow()
phrase_id = self.flashcard_ids_pool[row] phrase_id = self.flashcard_ids_pool[row]
self.load_flashcard_by_id(phrase_id) self.load_flashcard_by_id(phrase_id)
@ -560,7 +644,6 @@ class SpanishTrainerApp(QMainWindow):
# 🔊 AUDIO ENGINE & EXPANDED FLASHCARD ACTIONS # 🔊 AUDIO ENGINE & EXPANDED FLASHCARD ACTIONS
# ===================================================================== # =====================================================================
def load_flashcard_by_id(self, phrase_id): def load_flashcard_by_id(self, phrase_id):
"""Sets internal execution contexts cleanly targeting a specific card reference."""
conn = get_connection() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute("""
@ -578,7 +661,6 @@ class SpanishTrainerApp(QMainWindow):
self.lbl_card_meta.setText(f"Link ID: {record[0]} • Context: {record[2]} • Tag: {record[5]} • Deck: {record[3]}") 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): 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: if not self.flashcard_ids_pool:
QMessageBox.information(self, "Empty Pool", "No flashcards match your selected filter configurations.") QMessageBox.information(self, "Empty Pool", "No flashcards match your selected filter configurations.")
return return
@ -586,7 +668,6 @@ class SpanishTrainerApp(QMainWindow):
import random import random
target_id = random.choice(self.flashcard_ids_pool) target_id = random.choice(self.flashcard_ids_pool)
# Highlight matching row inside left panel layout matrix for context tracking
try: try:
matched_idx = self.flashcard_ids_pool.index(target_id) matched_idx = self.flashcard_ids_pool.index(target_id)
self.review_table.setCurrentCell(matched_idx, 0) self.review_table.setCurrentCell(matched_idx, 0)
@ -596,7 +677,6 @@ class SpanishTrainerApp(QMainWindow):
self.load_flashcard_by_id(target_id) self.load_flashcard_by_id(target_id)
def handle_play_voice(self): def handle_play_voice(self):
"""Plays or generates the Castilian neural track dynamically on fallback request loops."""
if not self.current_flashcard_id: if not self.current_flashcard_id:
return return
conn = get_connection() conn = get_connection()
@ -611,7 +691,6 @@ class SpanishTrainerApp(QMainWindow):
os.makedirs("media", exist_ok=True) os.makedirs("media", exist_ok=True)
target_file = f"media/{safe_name}_{lang}_female.mp3" target_file = f"media/{safe_name}_{lang}_female.mp3"
# Fault-Tolerance Loop: If Sandbox pass missed this track, generate it seamlessly right here
if not os.path.exists(target_file): if not os.path.exists(target_file):
print(f"🔊 Review Fallback: Synthesizing missing audio asset on the fly for '{text_str}'...") print(f"🔊 Review Fallback: Synthesizing missing audio asset on the fly for '{text_str}'...")
try: try:
@ -703,21 +782,21 @@ class SpanishTrainerApp(QMainWindow):
# 📦 ARTIFACT EXPORT GATEWAYS (ANKI & DEPLOYMENT CODES) # 📦 ARTIFACT EXPORT GATEWAYS (ANKI & DEPLOYMENT CODES)
# ===================================================================== # =====================================================================
def handle_export_anki_deck(self): def handle_export_anki_deck(self):
"""Action handler placeholder loop for your upcoming genanki package deployment modules.""" """Action handler loop for genanki package deployment modules."""
QMessageBox.information( QMessageBox.information(
self, "Anki Export Engine", self, "Anki Export Engine",
f"Staging packaging manifest for active view subset!\n\n" f"Staging packaging manifest for active view subset!\n\n"
f"Currently Filtered Scope count: {len(self.flashcard_ids_pool)} cards.\n" f"Target Directory: {self.anki_export_dir}\n"
f"Target Destination output: Castilian_Spanish_Workspace.apkg" f"Currently Filtered Scope count: {len(self.flashcard_ids_pool)} cards."
) )
def handle_export_video_assets(self): def handle_export_video_assets(self):
"""Action handler placeholder loop for compiling video cards.""" """Action handler loop for compiling video cards."""
QMessageBox.information( QMessageBox.information(
self, "Video Synthesis Suite", self, "Video Synthesis Suite",
f"Staging visual timeline render frames loop!\n\n" f"Staging visual timeline render frames loop!\n\n"
f"Currently Filtered Scope count: {len(self.flashcard_ids_pool)} sequences.\n" f"Target Directory: {self.video_export_dir}\n"
f"Target Audio Tracks bound: edge-tts neural assets map." f"Currently Filtered Scope count: {len(self.flashcard_ids_pool)} sequences."
) )
# ===================================================================== # =====================================================================

Binary file not shown.