before adding grammar field, adding notes field into the translation table
This commit is contained in:
parent
b3104d66a5
commit
985bbb5839
6 changed files with 3389 additions and 406 deletions
|
|
@ -1,121 +1,84 @@
|
|||
# core/bulk_importer.py
|
||||
import os
|
||||
import re
|
||||
from database.connection import get_connection
|
||||
from docling.document_converter import DocumentConverter
|
||||
from database.connection import get_connection
|
||||
|
||||
class BulkImporter:
|
||||
def __init__(self):
|
||||
self.converter = DocumentConverter()
|
||||
self.unit_regex = re.compile(r'U([0-9]+)')
|
||||
def import_pdf_glossary(self, pdf_path: str, textbook_name: str):
|
||||
"""Uses Docling layout extraction engine to parse tables out of multi-column glossary PDFs."""
|
||||
print(f"🔄 Analyzing structural layout for '{pdf_path}'...")
|
||||
|
||||
def clean_field(self, text: str) -> str:
|
||||
"""Removes layout spacing, structural artifacts, and noise."""
|
||||
if not text:
|
||||
return ""
|
||||
# Remove markdown bold/italics markers if any, and compress spaces
|
||||
cleaned = text.replace('*', '').replace('_', '').replace('\\', '')
|
||||
cleaned = re.sub(r'\s+', ' ', cleaned)
|
||||
return cleaned.strip()
|
||||
|
||||
def parse_unit(self, context_str: str) -> int:
|
||||
"""Extracts the integer unit ID out of strings like 'U3_8B' or 'U7'."""
|
||||
match = self.unit_regex.search(context_str)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return None
|
||||
|
||||
def insert_pair(self, cursor, es_text, en_text, context_tag, textbook_name):
|
||||
"""Helper to cleanly insert a single Spanish-English translation pair."""
|
||||
es_clean = self.clean_field(es_text)
|
||||
en_clean = self.clean_field(en_text)
|
||||
tag_clean = self.clean_field(context_tag)
|
||||
|
||||
# Guard clause against column headers or empty layout cells
|
||||
if not es_clean or not en_clean:
|
||||
return False
|
||||
if es_clean.lower() in ["spanish", "word", "alphabetical glossary", "es", "en"] or len(es_clean) <= 1:
|
||||
return False
|
||||
|
||||
unit_number = self.parse_unit(tag_clean)
|
||||
|
||||
try:
|
||||
# 1. Insert Spanish Phrase
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, textbook, unit, source_context)
|
||||
VALUES (?, 'es', ?, ?, ?)
|
||||
""", (es_clean, textbook_name, unit_number, tag_clean))
|
||||
es_id = cursor.lastrowid
|
||||
|
||||
# 2. Insert English Translation
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, textbook, unit, source_context)
|
||||
VALUES (?, 'en', ?, ?, ?)
|
||||
""", (en_clean, textbook_name, unit_number, tag_clean))
|
||||
en_id = cursor.lastrowid
|
||||
|
||||
# 3. Create Bidirectional Cross-Reference Entries
|
||||
cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (es_id, en_id))
|
||||
cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (en_id, es_id))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def import_pdf_glossary(self, file_path: str, textbook_name: str):
|
||||
print(f"🔄 Docling is analyzing structural layout for '{file_path}'...")
|
||||
|
||||
try:
|
||||
result = self.converter.convert(file_path)
|
||||
document_text = result.document.export_to_markdown()
|
||||
except Exception as e:
|
||||
print(f"❌ Docling processing failed: {e}")
|
||||
return 0
|
||||
if not os.path.exists(pdf_path):
|
||||
print(f"❌ Target document path could not be found: {pdf_path}")
|
||||
return
|
||||
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("📥 Parsing 6-column text grid structure and writing to database...")
|
||||
count = 0
|
||||
|
||||
# Process the markdown line-by-line
|
||||
lines = document_text.split('\n')
|
||||
for line in lines:
|
||||
line_str = line.strip()
|
||||
|
||||
# Target lines containing table formatting row data
|
||||
if not line_str.startswith('|') or not line_str.endswith('|'):
|
||||
continue
|
||||
|
||||
# Split by markdown table pipes
|
||||
# e.g., "| años60 m | 1960s | U8_7A | bañ osmpl | bath | U1_1A |"
|
||||
parts = [p.strip() for p in line_str.split('|')]
|
||||
|
||||
# A valid row split will include empty items at the ends due to leading/trailing pipes
|
||||
# For a 6-column table, len(parts) should be at least 8 elements
|
||||
if len(parts) < 7:
|
||||
continue
|
||||
|
||||
# Skip Markdown table header separator rows: |---|---|...
|
||||
if '---' in parts[1]:
|
||||
continue
|
||||
|
||||
# Extract Left-Hand Column Group (Columns 1, 2, 3)
|
||||
es_left = parts[1]
|
||||
en_left = parts[2]
|
||||
unit_left = parts[3] if len(parts) >= 4 else ""
|
||||
|
||||
if self.insert_pair(cursor, es_left, en_left, unit_left, textbook_name):
|
||||
count += 1
|
||||
|
||||
# Extract Right-Hand Column Group (Columns 4, 5, 6)
|
||||
if len(parts) >= 7:
|
||||
es_right = parts[4]
|
||||
en_right = parts[5]
|
||||
unit_right = parts[6]
|
||||
|
||||
if self.insert_pair(cursor, es_right, en_right, unit_right, textbook_name):
|
||||
count += 1
|
||||
|
||||
# Reset staging tables to guarantee fresh data state
|
||||
cursor.execute("DELETE FROM translations")
|
||||
cursor.execute("DELETE FROM phrases")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"🎉 Bulk ingestion complete! Registered {count} matched glossary items.")
|
||||
return count
|
||||
|
||||
print("📥 Initializing Docling layout analysis engine...")
|
||||
try:
|
||||
# 1. Initialize Docling converter
|
||||
converter = DocumentConverter()
|
||||
result = converter.convert(pdf_path)
|
||||
|
||||
print("📥 Parsing extracted text tables and tabular structures...")
|
||||
raw_inserts_count = 0
|
||||
|
||||
# 2. Iterate through extracted tables found inside the layout structure
|
||||
for table_idx, table_element in enumerate(result.document.tables):
|
||||
# Convert Docling table data framework back to standard pandas-like dictionary arrays
|
||||
table_data = table_element.export_to_dataframe()
|
||||
|
||||
# Iterate rows while ensuring it's not looking at headers or broken table pieces
|
||||
for row_idx, row in table_data.iterrows():
|
||||
row_list = list(row)
|
||||
|
||||
# Ensure we have enough columns to look at your glossary structure (expecting 4-6 columns)
|
||||
if len(row_list) < 3:
|
||||
continue
|
||||
|
||||
# Extract positions out of the 6-column structure:
|
||||
# Usually: Col 0 = Spanish, Col 1 = English, Col 2 or 3 = Context/Unit tag
|
||||
es_raw = str(row_list[0]).strip() if row_list[0] else ""
|
||||
en_raw = str(row_list[1]).strip() if row_list[1] else ""
|
||||
context_raw = str(row_list[2]).strip() if row_list[2] else ""
|
||||
|
||||
# Ignore empty lines or column labels (like "Spanish", "Español", "English")
|
||||
if not es_raw or not en_raw or "español" in es_raw.lower() or "english" in en_raw.lower():
|
||||
continue
|
||||
|
||||
# Parse out unit identifiers from context strings (e.g. "U2_3A" gives unit = 2)
|
||||
unit_num = 1
|
||||
unit_match = re.search(r'U(\d+)', context_raw, re.IGNORECASE)
|
||||
if unit_match:
|
||||
unit_num = int(unit_match.group(1))
|
||||
|
||||
# 3. Stage Spanish item row
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, textbook, unit, source_context, word_type)
|
||||
VALUES (?, 'es', ?, ?, ?, 'phrase')
|
||||
""", (es_raw, textbook_name, unit_num, context_raw))
|
||||
|
||||
# 4. Stage English item row (immediately adjacent)
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, textbook, unit, source_context, word_type)
|
||||
VALUES (?, 'en', ?, ?, ?, 'phrase')
|
||||
""", (en_raw, textbook_name, unit_num, context_raw))
|
||||
|
||||
raw_inserts_count += 2
|
||||
|
||||
conn.commit()
|
||||
print(f"🎉 Bulk ingestion staging complete! Staged {raw_inserts_count} entries.")
|
||||
|
||||
except Exception as docling_error:
|
||||
print(f"❌ Docling encountered a processing failure: {docling_error}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
conn.close()
|
||||
|
|
@ -4,45 +4,27 @@ from database.connection import get_connection
|
|||
|
||||
class GlossaryCleaner:
|
||||
def __init__(self):
|
||||
# Captures verb irregular brackets: " (zc)", " (ie)", etc.
|
||||
self.verb_pattern = re.compile(r'\s*\(([a-z\s]+)\)')
|
||||
|
||||
# Aggressive character-level match for broken trailing gender tags
|
||||
# Tracks variations like: " m", " f", "mpl", "fpl", "smpl", "osmpl" at the end of a string
|
||||
self.broken_gender_pattern = re.compile(r'[\s]*\b(m|f|pl)\b$|[\s\w]*(m|f|pl|mpl|fpl|smpl|osmpl)$', re.IGNORECASE)
|
||||
|
||||
def clean_and_expand_spanish(self, text: str) -> tuple[list[str], str, str]:
|
||||
"""Parses and strips layout noise from Spanish strings, extracting rich metadata."""
|
||||
def clean_and_expand_spanish(self, text: str) -> list[tuple[str, str, str]]:
|
||||
word_type = "phrase"
|
||||
grammar_note = None
|
||||
clean_text = text.strip()
|
||||
clean_text = str(text).strip()
|
||||
|
||||
# 1. Extract verb irregularities if present
|
||||
verb_match = self.verb_pattern.search(clean_text)
|
||||
if verb_match:
|
||||
word_type = "verb"
|
||||
grammar_note = verb_match.group(1).strip()
|
||||
clean_text = self.verb_pattern.sub('', clean_text).strip()
|
||||
|
||||
# 2. Extract and remove sticky gender notations (e.g., "bañ osmpl" -> "baños")
|
||||
# Check if text ends with common markers
|
||||
lower_text = clean_text.lower()
|
||||
if lower_text.endswith(('mpl', 'fpl', 'smpl', 'osmpl')):
|
||||
word_type = "noun"
|
||||
if 'f' in lower_text:
|
||||
grammar_note = "f, pl"
|
||||
else:
|
||||
grammar_note = "m, pl"
|
||||
|
||||
# Reconstruct the original word base before the layout break
|
||||
# e.g., "bañ osmpl" -> remove "osmpl" and append "os" to restore "baños"
|
||||
if lower_text.endswith('osmpl') and clean_text.lower().endswith('osmpl'):
|
||||
grammar_note = "f, pl" if 'f' in lower_text else "m, pl"
|
||||
if lower_text.endswith('osmpl'):
|
||||
clean_text = clean_text[:-5] + "os"
|
||||
else:
|
||||
# General strip of trailing garbage characters
|
||||
clean_text = re.sub(r'\s*[a-zA-Z\s]*$', '', clean_text)
|
||||
|
||||
# Standard boundary check for clean tags (e.g., "años60 m")
|
||||
elif re.search(r'\s+\b(m|f|pl|m,\s*pl|f,\s*pl)\b\s*$', clean_text):
|
||||
word_type = "noun"
|
||||
match = re.search(r'\b(m|f|pl|m,\s*pl|f,\s*pl)\b\s*$', clean_text)
|
||||
|
|
@ -50,102 +32,81 @@ class GlossaryCleaner:
|
|||
grammar_note = match.group(1).strip()
|
||||
clean_text = re.sub(r'\s+\b(m|f|pl|m,\s*pl|f,\s*pl)\b\s*$', '', clean_text).strip()
|
||||
|
||||
clean_text = clean_text.strip()
|
||||
|
||||
# 3. Expand dual-gender adjectives (e.g., "apasionado/a")
|
||||
variants = []
|
||||
if '/' in clean_text and ' ' not in clean_text:
|
||||
word_type = "adjective"
|
||||
base, suffix = clean_text.split('/', 1)
|
||||
base = base.strip()
|
||||
suffix = suffix.strip()
|
||||
|
||||
if suffix == 'a' and base.endswith('o'):
|
||||
variants.append((base, word_type, "m"))
|
||||
variants.append((base[:-1] + 'a', word_type, "f"))
|
||||
elif suffix == 'ra' and base.endswith('r'):
|
||||
variants.append((base, word_type, "m"))
|
||||
variants.append((base + 'a', word_type, "f"))
|
||||
else:
|
||||
variants.append((clean_text, word_type, grammar_note))
|
||||
else:
|
||||
variants.append((clean_text, word_type, grammar_note))
|
||||
|
||||
return variants
|
||||
return [(clean_text.strip(), word_type, grammar_note)]
|
||||
|
||||
def clean_english_text(self, text: str) -> str:
|
||||
"""Fixes layout spacing issues on English infinitive verbs."""
|
||||
cleaned = text.strip()
|
||||
# Ensure duplicate internal spacing is compressed
|
||||
cleaned = str(text).strip()
|
||||
cleaned = re.sub(r'\s+', ' ', cleaned)
|
||||
|
||||
# Intercept smashed English infinitives (e.g., "toappear" -> "to appear")
|
||||
if cleaned.startswith("to") and len(cleaned) > 2 and not cleaned.startswith("to "):
|
||||
cleaned = re.sub(r'^to([a-z])', r'to \1', cleaned)
|
||||
|
||||
return cleaned
|
||||
|
||||
def process_database_clean(self):
|
||||
"""Processes and normalizes raw table entries into pristine structures."""
|
||||
"""Processes raw entries from the source tables and builds translation row connections."""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("🧹 Extracting raw dataset for deep metadata extraction...")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT t.source_phrase_id, p1.text as es_text, p2.text as en_text,
|
||||
p1.textbook, p1.unit, p1.source_context
|
||||
FROM translations t
|
||||
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
||||
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
||||
WHERE p1.language = 'es'
|
||||
""")
|
||||
raw_rows = cursor.fetchall()
|
||||
|
||||
if not raw_rows:
|
||||
print("⚠️ No base phrases found to clean.")
|
||||
# Pull raw rows populated during BulkImporter phase
|
||||
# Ensuring we look at your base uncleaned layout records
|
||||
try:
|
||||
cursor.execute("SELECT id, text, language, textbook, unit, source_context FROM phrases")
|
||||
raw_rows = cursor.fetchall()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error reading raw phrases: {e}")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
print(f"⚙️ Migrating {len(raw_rows)} rows into corrected schemas...")
|
||||
# Separate them logically to reconstruct pairing matrices safely
|
||||
# We find adjacent lines assuming index N is Spanish and N+1 is its English partner
|
||||
paired_rows = []
|
||||
for i in range(0, len(raw_rows) - 1, 2):
|
||||
if raw_rows[i][2] == 'es':
|
||||
paired_rows.append((
|
||||
raw_rows[i][1], # Spanish raw text
|
||||
raw_rows[i+1][1], # English raw text
|
||||
raw_rows[i][3], # Textbook name
|
||||
raw_rows[i][4], # Unit Integer
|
||||
raw_rows[i][5] # Context string (e.g. U2_3A)
|
||||
))
|
||||
|
||||
# Clear out previous passes completely
|
||||
# Clear down active runtime tables to map fresh structural pairs
|
||||
cursor.execute("DELETE FROM translations")
|
||||
cursor.execute("DELETE FROM phrases")
|
||||
cursor.execute("DELETE FROM audio_tracks")
|
||||
|
||||
print(f"⚙️ Migrating {len(paired_rows)} raw rows into translation pairs...")
|
||||
inserted_count = 0
|
||||
|
||||
for _, es_raw, en_raw, textbook, unit, context in raw_rows:
|
||||
# Process Spanish layout text elements
|
||||
for es_raw, en_raw, textbook, unit, context in paired_rows:
|
||||
# Explicitly keep underscore formatting (e.g., U2_3A) intact
|
||||
safe_context = str(context).strip() if context else f"U{unit}"
|
||||
|
||||
# Clean both sides through individual language filters
|
||||
es_variants = self.clean_and_expand_spanish(es_raw)
|
||||
# Process and restore spaces to English text elements
|
||||
en_clean = self.clean_english_text(en_raw)
|
||||
|
||||
for es_clean, w_type, g_note in es_variants:
|
||||
try:
|
||||
# Insert pristine Spanish entry
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, textbook, unit, source_context, word_type, grammar_note)
|
||||
VALUES (?, 'es', ?, ?, ?, ?, ?)
|
||||
""", (es_clean, textbook, unit, context, w_type, g_note))
|
||||
es_id = cursor.lastrowid
|
||||
# 1. Store Spanish Text Item Node
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, textbook, unit, source_context, word_type, grammar_note)
|
||||
VALUES (?, 'es', ?, ?, ?, ?, ?)
|
||||
""", (es_clean, textbook, unit, safe_context, w_type, g_note))
|
||||
es_id = cursor.lastrowid
|
||||
|
||||
# Insert spaced English entry
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, textbook, unit, source_context, word_type, grammar_note)
|
||||
VALUES (?, 'en', ?, ?, ?, ?, NULL)
|
||||
""", (en_clean, textbook, unit, context, w_type))
|
||||
en_id = cursor.lastrowid
|
||||
# 2. Store English Text Item Node
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, textbook, unit, source_context, word_type, grammar_note)
|
||||
VALUES (?, 'en', ?, ?, ?, ?, NULL)
|
||||
""", (en_clean, textbook, unit, safe_context, w_type))
|
||||
en_id = cursor.lastrowid
|
||||
|
||||
# Re-map relations
|
||||
cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (es_id, en_id))
|
||||
cursor.execute("INSERT INTO translations (source_phrase_id, target_phrase_id) VALUES (?, ?)", (en_id, es_id))
|
||||
# 3. Form unique single translation row mapping bond
|
||||
cursor.execute("""
|
||||
INSERT INTO translations (source_phrase_id, target_phrase_id, deck_name)
|
||||
VALUES (?, ?, 'General')
|
||||
""", (es_id, en_id))
|
||||
|
||||
inserted_count += 1
|
||||
except Exception:
|
||||
continue
|
||||
inserted_count += 1
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"🎉 Schema extraction pipeline completed! Saved {inserted_count} pristine phrase definitions.")
|
||||
print(f"🎉 Schema extraction pipeline completed! Saved {inserted_count} pristine translation pairs.")
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
# database/connection.py
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def get_connection():
|
||||
return sqlite3.connect("spanish_trainer.db")
|
||||
# Force the path to be absolute relative to the project folder
|
||||
db_path = os.path.abspath("spanish_trainer.db")
|
||||
return sqlite3.connect(db_path)
|
||||
|
||||
def init_db():
|
||||
print("🛠️ Constructing relational database schema...")
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 1. Main Phrases Table
|
||||
# Enable foreign keys explicitly for this connection instance
|
||||
cursor.execute("PRAGMA foreign_keys = ON;")
|
||||
|
||||
# 1. Phrases Table (Holds individual localized text strings)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS phrases (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
|
@ -21,34 +28,38 @@ def init_db():
|
|||
grammar_note TEXT,
|
||||
voice_gender TEXT DEFAULT 'female',
|
||||
base_speed REAL DEFAULT 1.0,
|
||||
deck_name TEXT DEFAULT 'General',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
);
|
||||
""")
|
||||
|
||||
# 2. Translations Cross-Reference Table
|
||||
# 2. Translations Table (The relational tie binding English and Spanish IDs together)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS translations (
|
||||
translation_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_phrase_id INTEGER,
|
||||
target_phrase_id INTEGER,
|
||||
PRIMARY KEY (source_phrase_id, target_phrase_id),
|
||||
deck_name TEXT DEFAULT 'General',
|
||||
FOREIGN KEY (source_phrase_id) REFERENCES phrases(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (target_phrase_id) REFERENCES phrases(id) ON DELETE CASCADE
|
||||
)
|
||||
);
|
||||
""")
|
||||
|
||||
# 3. Audio Tracks Metadata Table (Required by the glossary cleaner)
|
||||
# 3. Audio Tracks Table (Links phrase items to local disk storage clips)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS audio_tracks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phrase_id INTEGER,
|
||||
file_path TEXT NOT NULL,
|
||||
sample_rate INTEGER,
|
||||
duration REAL,
|
||||
FOREIGN KEY (phrase_id) REFERENCES phrases(id) ON DELETE CASCADE
|
||||
)
|
||||
);
|
||||
""")
|
||||
|
||||
# CRITICAL: Force SQLite to physically commit the table architectures to disk
|
||||
conn.commit()
|
||||
|
||||
# Verification Sweep: Double-check that tables actually exist before we hand over control
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
print("✅ Rich metadata database schema initialized successfully.")
|
||||
|
||||
print(f"✅ Database tables physically confirmed on disk: {tables}")
|
||||
707
exported_phases.csv
Normal file
707
exported_phases.csv
Normal file
|
|
@ -0,0 +1,707 @@
|
|||
1,aparecer,"to appear",verb,U3_6A,General
|
||||
2,apasionada,passionate,phrase,U5_15C,General
|
||||
3,apellido,surname,noun,U1_4A,General
|
||||
4,aprender,"to learn",phrase,U2_2C,General
|
||||
5,aproximadamente,approximately,phrase,U3_2A,General
|
||||
6,apuntar,"to note down",phrase,U9_5A,General
|
||||
7,"aquí",here,phrase,U3_2A,General
|
||||
8,"aquí tiene","here you go",phrase,U4_GyC,General
|
||||
9,"árabe",Arabic,noun,U2_LEX,General
|
||||
10,archivo,file,noun,U9_7A,General
|
||||
11,"área",area,noun,U3_2A,General
|
||||
12,arena,sand,noun,U3_4A,General
|
||||
13,arepa,arepa,noun,U7_9A,General
|
||||
14,argentina,Argentinian,phrase,U1_3A,General
|
||||
15,"árido/a",arid,phrase,U3_LEX,General
|
||||
16,arma,weapon,noun,U3_2A,General
|
||||
17,arroz,rice,noun,U7_LEX,General
|
||||
18,arquitecta,architect,phrase,U1_6,General
|
||||
19,arquitectura,architecture,noun,U1_3A,General
|
||||
20,arte,art,noun,U1_1A,General
|
||||
21,artesanal,artisanal,phrase,U4_1A,General
|
||||
22,"artesanía f artista","crafts artist",phrase,U4_1A,General
|
||||
23,asado/a,roasted,phrase,U7_7A,General
|
||||
24,Asia,Asia,phrase,U3_LEX,General
|
||||
25,"aspecto físico","physical feature",noun,U5_14A,General
|
||||
26,aspecto,aspect,noun,U3_11B,General
|
||||
27,"Asunción",Asuncion,phrase,U3_13A,General
|
||||
28,"atención",takenote,phrase,U4_14A,General
|
||||
29,atender,"to lookafter",verb,U9_3A,General
|
||||
30,atento/a,attentive,phrase,U9_5A,General
|
||||
31,"atlántico/a",Atlantic,phrase,U3_6A,General
|
||||
32,atractivo/a,attractive,phrase,U8_7A,General
|
||||
33,"atraído/a",attracted,phrase,U9_3A,General
|
||||
34,"atún",tuna,noun,U7_2A,General
|
||||
35,"autobús",bus,noun,U3_4A,General
|
||||
36,"automático/a",automatic,phrase,U8_2A,General
|
||||
37,"autónomo/a",self-employed,phrase,U3_2A,General
|
||||
38,avenida,avenue,noun,U8_3A,General
|
||||
39,aventurero/a,adventurous,phrase,U5_3C,General
|
||||
40,"avión",plane,noun,U6_GyC,General
|
||||
41,"azúcar m azul","sugar blue",phrase,"U7_6A U4_2A",General
|
||||
42,"azul claro","light blue",phrase,U4_13A,General
|
||||
43,bailar,"to dance",phrase,U2_2C,General
|
||||
44,"bailarín/ina",dancer,phrase,U5_LEX,General
|
||||
45,baile,dance,noun,U6_10A,General
|
||||
46,bajito/a,short,phrase,U5_9A,General
|
||||
47,bajo,groundfloor,noun,U9_1A,General
|
||||
48,balalaica,balalaika,noun,U1_7A,General
|
||||
49,banco,bank,noun,U1_LEX,General
|
||||
50,bandera,flag,noun,U2_10B,General
|
||||
51,"bañador",swimsuit,noun,U4_3A,General
|
||||
52,"bañarse","to goforaswim",phrase,U9_GyC,General
|
||||
53,"Cádiz",Cadiz,phrase,U3_11B,General
|
||||
54,"café",coffee,noun,U3_2A,General
|
||||
55,"café con leche",coffeewithmilk,noun,U7_6A,General
|
||||
56,"café solo",espresso,noun,U7_4C,General
|
||||
57,cafetal,"coffee plantation",noun,U3_2A,General
|
||||
58,"cajero automático",cashmachine,noun,U8_2A,General
|
||||
59,"calabacín",courgette,noun,U7_LEX,General
|
||||
60,calabaza,pumpkin,noun,U7_LEX,General
|
||||
61,calamar,squid,noun,U7_2A,General
|
||||
62,"calcetín",sock,noun,U4_13B,General
|
||||
63,calidad,quality,noun,U3_2A,General
|
||||
64,"cálido/a",warm,phrase,U3_LEX,General
|
||||
65,caliente,hot,phrase,U6_11B,General
|
||||
66,calle,street,noun,U1_1A,General
|
||||
67,"calle peatonal","pedestrian street",noun,U8_3A,General
|
||||
68,calmado/a,calm,phrase,U5_15C,General
|
||||
69,calor,hot,noun,U3_4A,General
|
||||
70,calvo/a,bald,phrase,U5_LEX,General
|
||||
71,camarera,waitress,phrase,U1_4A,General
|
||||
72,camello,camel,noun,U3_9,General
|
||||
73,camino,road/journey,noun,U3_1,General
|
||||
74,Caminode,"Way of Saint James",phrase,U1,General
|
||||
75,"Santiago m camisa",shirt,noun,"U3_2A U4_2A",General
|
||||
76,camiseta,t-shirt,noun,U4_2A,General
|
||||
77,campamento,camping,noun,U5_8A,General
|
||||
78,campo,countryside,noun,U3_2A,General
|
||||
79,"Canadá",Canada,phrase,U1_3B,General
|
||||
80,canadiense,Canadian,phrase,U1_3A,General
|
||||
81,"canal de televisión","television channel",noun,U1_3C,General
|
||||
82,"canción",song,noun,U2_11B,General
|
||||
83,canela,cinnamon,noun,U7_8A,General
|
||||
84,cansado/a,tired,phrase,U6_2A,General
|
||||
85,cantante,singer,phrase,U5_4A,General
|
||||
86,cantar,"to sing",phrase,U5_5A,General
|
||||
87,cantidad,amount,noun,U3_6C,General
|
||||
88,canto,song,noun,U6_LEX,General
|
||||
89,"caña",smalldraughtbeer,noun,U2_13A,General
|
||||
90,capital,capital,noun,U3_1,General
|
||||
91,Caracas,Caracas,phrase,U3_GyC,General
|
||||
92,"carácter",personality,noun,U5_14A,General
|
||||
93,"característica",characteristics,noun,U7_12A,General
|
||||
94,"cargador de móvil",phonecharger,noun,U4_3A,General
|
||||
95,Caribe,Caribbean,phrase,U3_8C,General
|
||||
96,"cariñoso/a",caring,phrase,U9_12C,General
|
||||
97,carnaval,carnival,noun,U3_11B,General
|
||||
98,carne,meat,noun,U3_5C,General
|
||||
99,"carné de conducir","driving license",noun,U4_4A,General
|
||||
100,"carné deidentidad",IDcard,noun,U4_3A,General
|
||||
101,caro/a,expensive,phrase,U4_6A,General
|
||||
102,carta,menu,noun,U7_GyC,General
|
||||
103,"Cartagena de Indias",CartagenadeIndias,phrase,U2_10B,General
|
||||
104,"casa f casa rural","house houseinthecountry",noun,U1_8C,General
|
||||
105,"casado/a casarse","married togetmarried",phrase,"U5_7C U9_GyC",General
|
||||
106,"casco antiguo",oldtown,noun,U3_2A,General
|
||||
107,"clásico/a",classic,phrase,U4_6C,General
|
||||
108,clave,key,noun,U6_12A,General
|
||||
109,cliente/a,customer,phrase,U4_9A,General
|
||||
110,clima,climate,noun,U3_3B,General
|
||||
111,cobre,copper,noun,U3_3B,General
|
||||
112,coche,car,noun,U2_4A,General
|
||||
113,cocido,stew,noun,U7_10A,General
|
||||
114,cocido/a,baked,phrase,U7_7A,General
|
||||
115,"cocido madrileño",Madridstew,noun,U7_12A,General
|
||||
116,"cocidomontañés",Cantabrianbeanstew,noun,U7_10A,General
|
||||
117,cocinar,"to cook",phrase,U2_2A,General
|
||||
118,cocinero/a,chef,phrase,U1_3A,General
|
||||
119,colocar,"to place",phrase,U4_14A,General
|
||||
120,Colombia,Colombia,phrase,U2_10B,General
|
||||
121,colombiano/a,Colombian,phrase,U1_5A,General
|
||||
122,colonia,colony,noun,U3_11B,General
|
||||
123,ColoniaTovar,ColoniaTovar,phrase,U3_11B,General
|
||||
124,colonial,colonial,phrase,U3_2A,General
|
||||
125,color,colour,noun,U2_10B,General
|
||||
126,comer,"to eat",phrase,U3_5C,General
|
||||
127,comercial,"sales representative",phrase,U1_LEX,General
|
||||
128,comerciante,shopkeeper,phrase,U9_11A,General
|
||||
129,"comilón/ona",foodlover,phrase,U6_13A,General
|
||||
130,como,like,phrase,U3_2A,General
|
||||
131,"cómo",how,phrase,U3_6A,General
|
||||
132,"¿cómo andas?","how are you doing?",phrase,U1_2A,General
|
||||
133,"¿cómoeres?",whatareyoulike?,phrase,U6_2A,General
|
||||
134,"¿cómoestás?",howareyou?,phrase,U0_3,General
|
||||
135,"¿cómolotomas?",howdoyoutakeit?,phrase,U7_6B,General
|
||||
136,"¿cómosedice...?",howdoyousay...?,phrase,U0_5A,General
|
||||
137,"¿cómoseescribe ...?",howdoyouspell...?,phrase,U0_6,General
|
||||
138,"¿cómose pronuncia...?",howdoyoupronounce...?,phrase,U0_5A,General
|
||||
139,"¿cómotellamas?",whatisyourname?,phrase,U0_1A,General
|
||||
140,comodidad,comfort,noun,U1,General
|
||||
141,"cómodo/a",comfortable,phrase,"U9_3A U4_2A",General
|
||||
142,"compañero/a",flatmate,phrase,U1,General
|
||||
143,"compañero/a de trabajo",workcolleague,phrase,U1,General
|
||||
144,compartir,"to share",phrase,"U2_3A U6_12A",General
|
||||
145,"competición",competition,noun,U9_10B,General
|
||||
146,compi,"flatmate (colloq.)",phrase,U9_7A,General
|
||||
147,completamente,completely,noun,U3_11B,General
|
||||
148,"composición compositor/a",composition,phrase,U9_6B,General
|
||||
149,comprar,composer,phrase,"U5_LEX U4_9A",General
|
||||
150,compras,shopping,noun,U2_2A,General
|
||||
151,comprender,"to understand engagement",phrase,U2_6A,General
|
||||
152,"compromiso m común",common,phrase,"U6_2A U2_3A",General
|
||||
153,"comunicado/a comunicarse",communicated,phrase,U8_1A,General
|
||||
154,comunicativo/a,talkative,phrase,U9_12C,General
|
||||
155,comunidad,autonomouscommunity,phrase,U1,General
|
||||
156,"¿cuánto cuesta?",howmuchdoesitcost?,phrase,U4_9A,General
|
||||
157,"¿cuánto es?",howmuchisit?,phrase,U7_4A,General
|
||||
158,"¿cuánto/a/os/as?",howmuch/howmany?,phrase,U3_6A,General
|
||||
159,"¿cuántos años",howoldareyou?,phrase,U1,General
|
||||
160,"tienes? cuatro",four,phrase,"U1_4B U6_11B",General
|
||||
161,Cuba,Cuba,phrase,U0_4A,General
|
||||
162,cubano/a,Cuban,phrase,U1_6,General
|
||||
163,cuchara,spoon,noun,U7_LEX,General
|
||||
164,cucharilla,teaspoon,noun,U7_LEX,General
|
||||
165,cuchillo,knife,noun,U7_LEX,General
|
||||
166,cuenta,bill,noun,U7_4A,General
|
||||
167,cuidar,"to takecareof",phrase,U6_3A,General
|
||||
168,cultural,cultural,phrase,U8_LEX,General
|
||||
169,"cumpleaños",birthday,noun,U4_4C,General
|
||||
170,curso,course,noun,U2_2A,General
|
||||
171,cuy,Guineapig,noun,U3_GyC,General
|
||||
172,"dar clases","to giveclasses",phrase,U9_11C,General
|
||||
173,"darse cuenta","to realise",phrase,U9_5A,General
|
||||
174,de,of,phrase,U1_1A,General
|
||||
175,"de acuerdo","all right",phrase,U7_6B,General
|
||||
176,"de cuadros",check,phrase,U4_5A,General
|
||||
177,"¿de dóndeeres?","where are you from?",phrase,U1_4B,General
|
||||
178,"de estilo colonial","Colonial style",phrase,U8_11D,General
|
||||
179,"de fuera",fromoutside/foreign,phrase,U9_11A,General
|
||||
180,"de primero","for firstcourse",phrase,U7_4A,General
|
||||
181,"de rayas",stripy,phrase,U4_2A,General
|
||||
182,"de segundo",forsecondcourse,phrase,U7_4A,General
|
||||
183,"de todas partes","from everywhere",phrase,U3_2A,General
|
||||
184,"de valor",valuable,phrase,U9_8A,General
|
||||
185,decidir,"to decide",phrase,U4_14A,General
|
||||
186,decir,"to say",verb,U2_3A,General
|
||||
187,"decisión",decision,noun,U9_3A,General
|
||||
188,declarado/a,declared,phrase,U3_14A,General
|
||||
189,"dedicar tiempo","to spendtime(onsomething)",phrase,U6_2A,General
|
||||
190,defecto,weakness/defect,noun,U9_4A,General
|
||||
191,definir,"to define",phrase,U9_5A,General
|
||||
192,dejar,"to leave",phrase,U9_3A,General
|
||||
193,"dejarse algo","to forgetsomething",phrase,U9_5A,General
|
||||
194,"del tiempo",atroomtemperature,phrase,U7_6A,General
|
||||
195,delgado/a,thin,phrase,U5_LEX,General
|
||||
196,demasiado/a,"to omuch",phrase,U8_3A,General
|
||||
197,"deporte m deportista","sport athlete",phrase,U3_11B,General
|
||||
198,derecha,right,noun,U8_5A,General
|
||||
199,desayunar,"to havebreakfast",phrase,U6_2A,General
|
||||
200,"descendiente desconectar","descendent todisconnect",phrase,"U3_11B U6_12A",General
|
||||
201,desde,since/from,phrase,U3_2A,General
|
||||
202,"desde hace",since,phrase,U9_3A,General
|
||||
203,desear,"to wantsomething",phrase,U4_9A,General
|
||||
204,desfile,parade,noun,U6_8A,General
|
||||
205,elegante,elegant,phrase,U4_2A,General
|
||||
206,elegido/a,chosen,phrase,U5_14A,General
|
||||
207,elegir,"to choose",verb,U9_2B,General
|
||||
208,"emblemático/a",iconic,phrase,U8_7A,General
|
||||
209,embutido,curedmeat,noun,U7_2A,General
|
||||
210,empanada,empanada,noun,U3_3B,General
|
||||
211,empezar,"to start",phrase,U6_5B,General
|
||||
212,empleado/a,employee,phrase,U4_13A,General
|
||||
213,emprendedor/a,enterprising,phrase,U9_4A,General
|
||||
214,empresa,company,noun,U1_LEX,General
|
||||
215,empresade,telecommunicationscompany,phrase,U1_LEX,General
|
||||
216,empresade,transportcompany,phrase,U1_LEX,General
|
||||
217,en,in/on,phrase,U0_6,General
|
||||
218,encambio,ontheotherhand,phrase,U8_9C,General
|
||||
219,enforma,inshape,phrase,U6_2A,General
|
||||
220,enpunto,"o'clock",phrase,U6_GyC,General
|
||||
221,"¿en quétrabajas?",whatdoyoudoforaliving?,phrase,U1_4B,General
|
||||
222,entodo,entirely,phrase,U3_5A,General
|
||||
223,enventa,"for sale",phrase,U9_3A,General
|
||||
224,enamorarsea,"to fall in love at first sight",phrase,U9_8A,General
|
||||
225,encantar,"to love",phrase,U5_3A,General
|
||||
226,encanto,charm,noun,U8_LEX,General
|
||||
227,enchilada,enchilada,noun,U3_6A,General
|
||||
228,"energía",energy,noun,U6_2A,General
|
||||
229,"enfermería",nursing,noun,U9_12B,General
|
||||
230,enfermero/a,nurse,phrase,U1_LEX,General
|
||||
231,enfermo/a,ill,phrase,U9_11A,General
|
||||
232,ensalada,salad,noun,U7_1A,General
|
||||
233,"ensalada mixta",mixedsalad,noun,U7_3A,General
|
||||
234,enseguida,comingrightup,phrase,U1,General
|
||||
235,entrante,starter,noun,U7_12C,General
|
||||
236,entresemana,duringtheweek,phrase,U6_2A,General
|
||||
237,equipaje,luggage,noun,U9_7C,General
|
||||
238,equipo,team,noun,U3_5A,General
|
||||
239,equivocarse,"to bewrongabout",phrase,U9_5A,General
|
||||
240,escolar,school,phrase,U4_13A,General
|
||||
241,escribir,"to write",phrase,U5_3A,General
|
||||
242,escuchar,"to listento",phrase,U2_2A,General
|
||||
243,escuela,school,noun,U1_1A,General
|
||||
244,escultura,sculpture,noun,U2_10B,General
|
||||
245,ese/a,this,phrase,U0_4C,General
|
||||
246,"España",Spain,phrase,U0_4A,General
|
||||
247,especial,special,phrase,U4_15B,General
|
||||
248,especializado/a,specialised,phrase,U7_6A,General
|
||||
249,espectacular,spectacular,phrase,U9_3A,General
|
||||
250,esperar,"to wait",phrase,U5_3A,General
|
||||
251,espinaca,spinach,noun,U7_3A,General
|
||||
252,"esquí",skiing,noun,U1_1A,General
|
||||
253,esquiar,"to ski",phrase,U9_6C,General
|
||||
254,esquina,corner,noun,U8_5A,General
|
||||
255,establecimiento,establishment,noun,U7_GyC,General
|
||||
256,"estación de metro",metrostation,phrase,U8_2A,General
|
||||
257,final,end,noun,U3_2A,General
|
||||
258,"físico/a",physical,phrase,U5_14A,General
|
||||
259,flamenco,flamenco,noun,U1_7A,General
|
||||
260,flan,eggcustard,noun,U7_3A,General
|
||||
261,flor,flower,noun,U6_8A,General
|
||||
262,forma,shape,noun,U3_11B,General
|
||||
263,foto,photo,noun,U2_LEX,General
|
||||
264,"fotografía",photography,noun,U5_3A,General
|
||||
265,"fotógrafo/a",photographer,phrase,U9_LEX,General
|
||||
266,"francés/esa",French,phrase,U1_3A,General
|
||||
267,frecuencia,frequency,noun,U6_2A,General
|
||||
268,fresa,strawberry,noun,U7_5C,General
|
||||
269,fresco/a,fresh,phrase,U7_2A,General
|
||||
270,frijoles,beans,noun,U7_LEX,General
|
||||
271,"frío/a",cold,phrase,U3_3B,General
|
||||
272,frito/a,fried,phrase,U7_3A,General
|
||||
273,fruta,fruit,noun,U7_3A,General
|
||||
274,frutadetemporada,"seasonal fruit",phrase,U7_3A,General
|
||||
275,"frutos secos",nuts,noun,U7_5C,General
|
||||
276,fuera,outside,phrase,U6_9D,General
|
||||
277,fumar,"to smoke",phrase,U6_3A,General
|
||||
278,fundado/a,founded,phrase,U3_2A,General
|
||||
279,fundamental,fundamental,phrase,U7_7A,General
|
||||
280,"fútbol",football,noun,U2_LEX,General
|
||||
281,futuro,future,noun,U2_12A,General
|
||||
282,"gafas de sol",sunglasses,noun,U4_3A,General
|
||||
283,"galería",shoppingcentre,noun,U4_1A,General
|
||||
284,Galicia,Galicia,phrase,U3_2A,General
|
||||
285,galleta,biscuit,noun,U6_11B,General
|
||||
286,gamba,prawn,noun,U7_1A,General
|
||||
287,ganar,"to win",phrase,U3_6A,General
|
||||
288,ganarunpremio,"to winaprize",phrase,U9_8A,General
|
||||
289,garbanzos,chickpeas,noun,U7_LEX,General
|
||||
290,gas,petrol,noun,U7_4A,General
|
||||
291,gasolinera,"petrol station",noun,U8_LEX,General
|
||||
292,gasto,expense,noun,U9_2B,General
|
||||
293,"gastronómico/a",culinary,phrase,U4_12A,General
|
||||
294,"gazpacho m",gazpacho,noun,U7_4A,General
|
||||
295,"geldebaño",showergel,phrase,U4_3A,General
|
||||
296,generoso/a,generous,phrase,U9_4A,General
|
||||
297,gente,people,noun,U2_13B,General
|
||||
298,"geográfico/a",geographical,phrase,U8_9A,General
|
||||
299,"geólogo/a",geology,phrase,U9_2B,General
|
||||
300,gimnasio,gym,noun,U1_LEX,General
|
||||
301,"ginecólogo/a",gynaecologist,phrase,U1_6,General
|
||||
302,girar,"to turn",phrase,U8_6C,General
|
||||
303,girasol,sunflower,noun,U7_LEX,General
|
||||
304,golf,golf,noun,U5_GyC,General
|
||||
305,gordo/a,fat,phrase,U5_LEX,General
|
||||
306,gorra,cap,noun,U4_5A,General
|
||||
307,"gorro m Gotemburgo","hat Gothenburg",phrase,"U4_LEX U3_8B",General
|
||||
308,gracias,thankyou,phrase,U0_6,General
|
||||
309,horario,schedule,phrase,U6_2A,General
|
||||
310,horno,oven,noun,U7_3A,General
|
||||
311,hortaliza,vegetable,noun,U7_2A,General
|
||||
312,hospital,hospital,noun,U1_LEX,General
|
||||
313,hospitalidad,hospitality,noun,U8_9A,General
|
||||
314,hostelero/a,hotelier,phrase,U9_11A,General
|
||||
315,hotel,hotel,noun,U1_1A,General
|
||||
316,hoy,"to day",phrase,U3_4A,General
|
||||
317,huerto,vegetablegarden,noun,U9_3A,General
|
||||
318,huevo,egg,noun,U7_2A,General
|
||||
319,humanidad,humanity,noun,U3_2A,General
|
||||
320,"húmedo/a",humid,phrase,U3_4A,General
|
||||
321,humor,mood,noun,U6_2A,General
|
||||
322,humus,hummus,noun,U7_2C,General
|
||||
323,idea,Iberian,noun,U3_2A,General
|
||||
324,ideal,"idea ideal",phrase,"U2_5A U1_1A",General
|
||||
325,"infusión",tea,noun,U3_GyC,General
|
||||
326,ingeniero/a,engineer,phrase,U1_6,General
|
||||
327,Inglaterra,England,phrase,U3_8C,General
|
||||
328,insociable,unsociable,phrase,U9_LEX,General
|
||||
329,instrumento,musicalinstrument,phrase,U1,General
|
||||
330,intercambio,exchange,noun,U2_LEX,General
|
||||
331,m,"to urist interest",phrase,U3_2A,General
|
||||
332,interesante,interesting,phrase,U2_5A,General
|
||||
333,interior,interior,noun,U4_3A,General
|
||||
334,intermediario/a,intermediary,phrase,U9_11A,General
|
||||
335,internacional,international,phrase,U1,General
|
||||
336,internet,internet,phrase,U2_11A,General
|
||||
337,invierno,winter,noun,U3_8B,General
|
||||
338,invitado/a,guest,phrase,U5_14B,General
|
||||
339,irdecompras,"to goshopping",phrase,U2_2A,General
|
||||
340,"ir de viaje","to gotravelling",phrase,U4_4A,General
|
||||
341,"idioma iglesia","important late incredible independent Indian infographic information",noun,U8_3A,General
|
||||
342,"ibérico/a identidad","impatient independence",noun,"U9_4A U3_4A",General
|
||||
343,"f impaciente",church,phrase,U1,General
|
||||
344,levantarse,"to getup",phrase,U6_1A,General
|
||||
345,libro,book,noun,U0_5A,General
|
||||
346,lila,purple,phrase,U4_LEX,General
|
||||
347,Lima,Lima,phrase,U8_7E,General
|
||||
348,"limón",lemon,noun,U7_6A,General
|
||||
349,limpieza,cleanliness,noun,U8_9A,General
|
||||
350,limpio/a,clean,phrase,U8_LEX,General
|
||||
351,lindo/a,cute,phrase,U3_6A,General
|
||||
352,"lingüista",linguist,phrase,U1_6,General
|
||||
353,liso/a,straight,phrase,U5_LEX,General
|
||||
354,lista,list,noun,U9_5A,General
|
||||
355,literatura,literature,noun,U2_1A,General
|
||||
356,llamado/a,called,phrase,U3_2A,General
|
||||
357,llamarse,"to becalled",phrase,U3_2A,General
|
||||
358,llave,key,noun,U9_2B,General
|
||||
359,llegar,"to arrive",phrase,U3_2A,General
|
||||
360,llevar,"to wear",phrase,U4_2A,General
|
||||
361,llevar,"to have",phrase,U7_2A,General
|
||||
362,llevarse,"to take",phrase,U4_9A,General
|
||||
363,llover,"to rain",verb,U3_4A,General
|
||||
364,lluvioso/a,rainy,phrase,U3_6A,General
|
||||
365,"loquemás",themost,phrase,U8_4D,General
|
||||
366,loquemenos,theleast,phrase,U8_4D,General
|
||||
367,"lo siento Londres","I'm sorry London",phrase,"U0_6 U5_2B",General
|
||||
368,luego,later,phrase,U3_4A,General
|
||||
369,lugar,place,noun,U1_4B,General
|
||||
370,lunes,Monday,noun,U3_4A,General
|
||||
371,luz,light,noun,U5_2A,General
|
||||
372,macarrones,macaroni,noun,U7_GyC,General
|
||||
373,madre,mother,noun,U5_2A,General
|
||||
374,Madrid,Madrid,phrase,U2_6A,General
|
||||
375,"madrileño/a",personfromMadrid,phrase,U7_12A,General
|
||||
376,madrugar,"to getupearly",phrase,U6_2A,General
|
||||
377,maestro/a,teacher,phrase,U9_4C,General
|
||||
378,"mágico/a",magical,phrase,U2_10B,General
|
||||
379,"maíz",corn,noun,U2_10B,General
|
||||
380,malcomunicado,poorlyconnected,phrase,U8_1A,General
|
||||
381,"Málaga",Malaga,phrase,U1_1A,General
|
||||
382,maleta,"suitcase Majorca",noun,U4_6C,General
|
||||
383,manera,way,phrase,U7_7A,General
|
||||
384,mangacorta,"short sleeve",noun,U4_2A,General
|
||||
385,"mangalarga f maniático/a",longsleeve,phrase,U4_2A,General
|
||||
386,mano,hand,noun,U4_1B,General
|
||||
387,manzana,apple,noun,U7_LEX,General
|
||||
388,"mañana",morning,phrase,U3_4A,General
|
||||
389,"mapa m","map worldmap",noun,U3_1,General
|
||||
390,"mapamundi mar m,f",sea,phrase,"U3_1 U5_3A",General
|
||||
391,"f marido","brand husband",noun,"U4_2A U5_7A",General
|
||||
392,marinero/a,sailor,phrase,U1,General
|
||||
393,miles,thousands,phrase,U3_2A,General
|
||||
394,"millón",million,phrase,U3_2A,General
|
||||
395,minuto,minute,noun,U6_2A,General
|
||||
396,mire,look,phrase,U4_9A,General
|
||||
397,mismo/a,same,phrase,U3_2A,General
|
||||
398,mixto/a,mixed,phrase,U7_3A,General
|
||||
399,mochila,backpack,noun,U0_5A,General
|
||||
400,moda,fashion,noun,U1_3A,General
|
||||
401,modelo,model,phrase,U1_GyC,General
|
||||
402,moderno/a,modern,phrase,U4_6C,General
|
||||
403,modo,way,noun,U3_6C,General
|
||||
404,"molino de viento",windmill,noun,U3_9,General
|
||||
405,momento,moment,noun,U6_1A,General
|
||||
406,moneda,currency,noun,U3_2A,General
|
||||
407,"montaña",mountain,noun,U3_1,General
|
||||
408,"montañés/esa",mountain,phrase,U7_10A,General
|
||||
409,montar,"to setup",phrase,U9_3A,General
|
||||
410,Montevideo,Montevideo,phrase,U3_13A,General
|
||||
411,monumento,landmark,noun,U3_2A,General
|
||||
412,moreno/a,dark-haired,phrase,U5_9A,General
|
||||
413,mostaza,mustard,noun,U7_2A,General
|
||||
414,"móvil",mobile,noun,U1_4B,General
|
||||
415,"mucho muchos/as","alot many/alotof",phrase,"U2_11B U2_7B",General
|
||||
416,mueble,furniture,noun,U8_8A,General
|
||||
417,mujer,woman,noun,U4_2A,General
|
||||
418,mundo,world,noun,U2_3A,General
|
||||
419,"unos pódcast","some podcasts",phrase,U2_9A-Extra,General
|
||||
420,museo,museum,noun,U1_1A,General
|
||||
421,"música",music,noun,U2_1A,General
|
||||
422,"música clásica",classicalmusic,noun,U5_4A,General
|
||||
423,"música electrónica",electronicmusic,noun,U5_4A,General
|
||||
424,"música envivo",livemusic,noun,U5_6A,General
|
||||
425,"música",indiemusic,phrase,U1,General
|
||||
426,"músicapop",popmusic,noun,U4_10A,General
|
||||
427,"música soul",soulmusic,noun,U5_4A,General
|
||||
428,musical,musical,phrase,U3_GyC,General
|
||||
429,"músico/a",musician,phrase,U5_2A,General
|
||||
430,muy,very,phrase,U2_5A,General
|
||||
431,O,O,phrase,O,General
|
||||
432,N,N,phrase,N,General
|
||||
433,nacer,"to beborn",verb,U3_GyC,General
|
||||
434,nachos,nachos,noun,U7_1A,General
|
||||
435,nacimiento,birth,noun,U5_2A,General
|
||||
436,"nacional nacionalidad","national nationality",noun,"U2_10B U1_3D",General
|
||||
437,nadar,"to swim",phrase,U1,General
|
||||
438,naranja,orange,noun,U7_LEX,General
|
||||
439,"naranja nativo/a","orange native",phrase,"U4_LEX U2_LEX",General
|
||||
440,natural,natural,phrase,U2_3A,General
|
||||
441,"naturaleza f Navarra","nature Navarre",phrase,"U2_1A U9_11A",General
|
||||
442,Navidad,Christmas,noun,U6_11B,General
|
||||
443,olvidar,"to forget",phrase,U9_5A,General
|
||||
444,"ópera",opera,noun,U5_5A,General
|
||||
445,opinar,"to haveanopinionon",phrase,U9_2B,General
|
||||
446,orden,order,noun,U6_11A,General
|
||||
447,ordenador,computer,noun,U0_5A,General
|
||||
448,"ordenadorportátil",laptop,noun,U4_3B,General
|
||||
449,organizado/a,organised,phrase,U6_9A,General
|
||||
450,organizar,"to organise",phrase,U9_3A,General
|
||||
451,origen,origin,noun,U1_4B,General
|
||||
452,original,original,phrase,U4_6C,General
|
||||
453,oso,bear,noun,U3_9,General
|
||||
454,Otavalo,Otavalo,phrase,U4_1A,General
|
||||
455,"otoño",autumn,noun,U3_8B,General
|
||||
456,otro/a,other,phrase,U0_2A,General
|
||||
457,paciente,patient,phrase,U9_4A,General
|
||||
458,"Pacífico",Pacific,noun,U3_4A,General
|
||||
459,padre,father,noun,U5_1A,General
|
||||
460,paella,paella,noun,U2_13A,General
|
||||
461,"página",page,noun,U0_6,General
|
||||
462,"páginaweb",website,noun,U2_7B,General
|
||||
463,"país","country countryside",noun,U2_3A,General
|
||||
464,paisaje,word,noun,U9_3A,General
|
||||
465,palacio,"palace PalmadeMallorca",noun,U3_2A,General
|
||||
466,"PalmadeMallorca palmera",palmtree,noun,"U4_1A U3_9",General
|
||||
467,pan,bread,noun,U7_1A,General
|
||||
468,panblanco,whitebread,noun,U7_LEX,General
|
||||
469,"Estoy subiendo las escaleras","I am going up the stairs",phrase,M1-phrase,General
|
||||
470,"Panamá",Panama,phrase,U7_9A,General
|
||||
471,"pantalón",trousers,noun,U4_3A,General
|
||||
472,pantalones,shorts,noun,U4_3A,General
|
||||
473,papelera,bin,noun,U0_5A,General
|
||||
474,para,for/inorderto,phrase,U2_9A,General
|
||||
475,paraempezar,"to start",phrase,U7_3A,General
|
||||
476,"paramí",forme,phrase,U4_2B,General
|
||||
477,"parada de autobús",busstop,noun,U8_2A,General
|
||||
478,paraguas,umbrella,noun,U9_5A,General
|
||||
479,Paraguay,Paraguay,phrase,U3_GyC,General
|
||||
480,"pareja f París","partner Paris",phrase,"U5_LEX U1_GyC",General
|
||||
481,parking,carpark,noun,U8_2A,General
|
||||
482,parque,park,noun,U2_10B,General
|
||||
483,"parque nacional",nationalpark,noun,U2_10B,General
|
||||
484,participar,"to participate",phrase,U6_8A,General
|
||||
485,particular,particular,phrase,U6_8A,General
|
||||
486,pasaporte,passport,noun,U4_4A,General
|
||||
487,pasar,"to spend",phrase,U2_8A,General
|
||||
488,"pasar de largo","to goby",phrase,U9_5A,General
|
||||
489,"paseo m",walk,noun,U4_12A,General
|
||||
490,"paseo acaballo pasión","horseriding passion",noun,"U4_12A U5_3A",General
|
||||
491,pasta,pasta,noun,U7_1C,General
|
||||
492,plato,dish,noun,U2_11A,General
|
||||
493,"plato principal",maincourse,noun,U7_3A,General
|
||||
494,"plato único",singlecourse,phrase,U7_12A,General
|
||||
495,playa,beach,noun,U0_6,General
|
||||
496,plaza,square,noun,U3_2A,General
|
||||
497,pleno/a,full,phrase,U6_2A,General
|
||||
498,"plurilingüe",plurilingual,phrase,U2_3A,General
|
||||
499,"población",population,noun,U3_2A,General
|
||||
500,poblado/a,populated,phrase,U3_2A,General
|
||||
501,poco,little,phrase,U3_8C,General
|
||||
502,poco/a/os/as,"little,few",phrase,U3_2A,General
|
||||
503,podcast,podcast,noun,U2_LEX,General
|
||||
504,poder,"to can",verb,U0_6,General
|
||||
505,"podríamos",wecould,phrase,U0_6,General
|
||||
506,poema,poem,noun,U5_10A,General
|
||||
507,"poesía",poetry,noun,U9_2B,General
|
||||
508,"policía",police,phrase,U1_GyC,General
|
||||
509,polideportivo,"sports centre",noun,U8_2A,General
|
||||
510,"polifacético/a",well-rounded,phrase,U9_12C,General
|
||||
511,pollo,chicken,noun,U7_2A,General
|
||||
512,poner,"to put",verb,U7_4A,General
|
||||
513,poplatino,Latinpop,noun,U5_4A,General
|
||||
514,pop-rock,poprock,noun,U5_4A,General
|
||||
515,popular,popular,phrase,U3_6A,General
|
||||
516,"por eso",forthatreason,phrase,U3_11B,General
|
||||
517,"por favor",please,phrase,U0_6,General
|
||||
518,"por fin",finally,phrase,U3_1,General
|
||||
519,"porlamañana/",inthemorning/atnight,phrase,U1,General
|
||||
520,"¿por qué?",why?,phrase,U2_12A,General
|
||||
521,porque,because,phrase,U2_9C,General
|
||||
522,"portátil",laptop,phrase,U4_3B,General
|
||||
523,"portugués/esa",Portuguese,phrase,U1_GyC,General
|
||||
524,postal,postcard,noun,U2_11A,General
|
||||
525,postre,dessert,noun,U7_3A,General
|
||||
526,"práctica",practice,noun,U6_12A,General
|
||||
527,practicar,"to practice",phrase,U2_6A,General
|
||||
528,"práctico/a",practical,phrase,U4_6C,General
|
||||
529,precio,price,noun,U4_9A,General
|
||||
530,precioso/a,beautiful,phrase,U3_4A,General
|
||||
531,preferencia,preference,noun,U6_2A,General
|
||||
532,preferido/a,favourite,phrase,U5_2A,General
|
||||
533,preferir,"to prefer",verb,U4_GyC,General
|
||||
534,pregunta,question,noun,U3_6A,General
|
||||
535,preguntar,"to ask",phrase,U1_4B,General
|
||||
536,premio,prize,noun,U6_13A,General
|
||||
537,prenda,itemofclothing,noun,U4_15B,General
|
||||
538,preparar,"to prepare",phrase,U6_8A,General
|
||||
539,primavera,spring,noun,U3_8B,General
|
||||
540,primero/a,first,phrase,U3_2A,General
|
||||
541,primo/a,cousin,phrase,U5_1A,General
|
||||
542,principal,main,phrase,U7_3A,General
|
||||
543,probar,"to try",phrase,U7_12C,General
|
||||
544,"producción",production,noun,U3_2A,General
|
||||
545,producto,product,noun,U3_3B,General
|
||||
546,productor,producer,noun,U3_7A,General
|
||||
547,"profesión",profession,noun,U1,General
|
||||
548,reparar,"to repair",phrase,U9_7A,General
|
||||
549,repetir,"to repeat",verb,U0_6,General
|
||||
550,repoblar,"to repopulate",verb,U9_3A,General
|
||||
551,"República",DominicanRepublic,phrase,U1,General
|
||||
552,res,beef,noun,U7_LEX,General
|
||||
553,"reserva natural",naturereserve,noun,U3_11B,General
|
||||
554,residencia,residence,noun,U2_12A,General
|
||||
555,residencial,residential,phrase,U8_11D,General
|
||||
556,responsable,responsible,phrase,U9_4A,General
|
||||
557,respuesta,answer,noun,U6_2A,General
|
||||
558,restaurante,restaurant,noun,U1_1A,General
|
||||
559,restos,remains,noun,U3_2A,General
|
||||
560,resultado,result,noun,U6_2A,General
|
||||
561,reunirse,"to gettogether",phrase,U6_8A,General
|
||||
562,"revisión médica",medicalcheck-up,noun,U9_11A,General
|
||||
563,revista,magazine,noun,U2_4A,General
|
||||
564,ribera,riverbed,noun,U8_3A,General
|
||||
565,"río",river,noun,U3_LEX,General
|
||||
566,"Río de Janeiro",RiodeJaneiro,phrase,U5_14B,General
|
||||
567,rizado/a,curly,phrase,U5_9A,General
|
||||
568,robar,"to steal",phrase,U5_2A,General
|
||||
569,rojo/a,red,phrase,U1,General
|
||||
570,"romántico/a",romantic,phrase,U4_6A,General
|
||||
571,ropa,clothes,noun,"U5_15C U1_10B",General
|
||||
572,"ropa interior",underwear,noun,U4_3A,General
|
||||
573,rosa,pink,phrase,U4_2A,General
|
||||
574,rosario,rosary,noun,U3_11B,General
|
||||
575,rubio/a,fair-haired,phrase,U5_9A,General
|
||||
576,ruidoso/a,noisy,phrase,U8_1A,General
|
||||
577,ruinas,ruins,noun,U3_4A,General
|
||||
578,rural,rural,phrase,U9_3A,General
|
||||
579,ruso,Russian,noun,U2_LEX,General
|
||||
580,"ruta gastronómica",foodtour,noun,U4_12A,General
|
||||
581,rutina,routine,noun,U6_12A,General
|
||||
582,"sábado",Saturday,noun,U2_2C,General
|
||||
583,saber,"to know",phrase,U5_2A,General
|
||||
584,sabor,taste,noun,U7_8A,General
|
||||
585,"Sáhara",Sahara,phrase,U3_8C,General
|
||||
586,sal,salt,noun,U3_11B,General
|
||||
587,salado/a,savoury,phrase,U7_9C,General
|
||||
588,salar,"salt flat",noun,U3_11B,General
|
||||
589,"salchichas f, Pl",sausages,phrase,U7_LEX,General
|
||||
590,salir,"to goout",verb,U2_2A,General
|
||||
591,"salir acenar salir con amigos","to gooutfordinner togooutwithfriends",phrase,"U2_2C U2_2C",General
|
||||
592,"salir de noche","to gooutatnight",phrase,U2_2C,General
|
||||
593,"salmón",salmon,phrase,U7_3A,General
|
||||
594,"salsa f",sauce,noun,U7_LEX,General
|
||||
595,"salsa brava salteado/a","spicysauce sautéed",phrase,"U7_12A U7_7A",General
|
||||
596,salto,waterfall,noun,U3_14C,General
|
||||
597,"sobre todo",aboveall,phrase,U7_6B,General
|
||||
598,sobrino/a,nephew/niece,phrase,U1,General
|
||||
599,sociable,sociable,phrase,"U5_1A U5_3C",General
|
||||
600,sol,sun,noun,U1_1A,General
|
||||
601,solar,sun,phrase,U4_3A,General
|
||||
602,soledad,solitude,noun,U2_1A,General
|
||||
603,solo,alone,phrase,U5_8A,General
|
||||
604,soltero/a,single,phrase,U5_7C,General
|
||||
605,sopa,soup,noun,U7_3A,General
|
||||
606,sostenible,sustainable,phrase,U4_15B,General
|
||||
607,soy,Iam,phrase,U1_3A,General
|
||||
608,soyyo,"It'sme",phrase,U1_2A,General
|
||||
609,su,its,phrase,U3_1,General
|
||||
610,sucio/a,dirty,phrase,U8_4A,General
|
||||
611,"sueño",sleepy,noun,U6_2A,General
|
||||
612,suficiente,enough,phrase,U7_7A,General
|
||||
613,suizo/a,Swiss,phrase,U1_6,General
|
||||
614,supermercado,supermarket,noun,U1_LEX,General
|
||||
615,sur,south,noun,U3_3B,General
|
||||
616,sureste,southeast,noun,U3_2A,General
|
||||
617,surf,surfing,noun,U4_12A,General
|
||||
618,suroeste,southwest,noun,U3_LEX,General
|
||||
619,sushi,sushi,noun,U1_7A,General
|
||||
620,tableta,tablet,noun,U0_5A,General
|
||||
621,taco,taco,noun,U7_9A,General
|
||||
622,"Tacuarembó",Tacuarembo,phrase,U3_13A,General
|
||||
623,Tailandia,Thailand,phrase,U3_12C,General
|
||||
624,talla,size,noun,U4_2A,General
|
||||
625,taller,workshop,noun,U1_3C,General
|
||||
626,tamal,tamale,noun,U3_4A,General
|
||||
627,"también","to o",phrase,U1_5A,General
|
||||
628,tampoco,either,phrase,U5_5A,General
|
||||
629,tango,tango,noun,U1_7,General
|
||||
630,Tanzania,Tanzania,phrase,U3_10B,General
|
||||
631,tapa,tapa,noun,U2_13A,General
|
||||
632,"tapón",earplugs,noun,U4_3B,General
|
||||
633,tarde,afternoon,phrase,U0_6,General
|
||||
634,tarde-noche,evening,noun,U6_2A,General
|
||||
635,Tarifa,Tarifa,phrase,U3_8A,General
|
||||
636,tarjeta,card,noun,U4_3A,General
|
||||
637,"tarjeta de crédito","credit card",phrase,U4_3A,General
|
||||
638,tarta,cake,noun,U7_5C,General
|
||||
639,taxi,taxi,noun,U1_1A,General
|
||||
640,taza,mug,noun,U7_LEX,General
|
||||
641,"té",tea,noun,U5_GyC,General
|
||||
642,teatro,theatre,phrase,U2_2A,General
|
||||
643,"m tejido",material/cloth,noun,U4_2A,General
|
||||
644,tela,fabric,noun,U9_10B,General
|
||||
645,"teleférico","cable car telephone",noun,"U3_14C U1_4A",General
|
||||
646,"teléfono m templado/a",mild,phrase,U1,General
|
||||
647,templo,temple,noun,U3_4A,General
|
||||
648,"temporada f temprano","seasonal early",phrase,U7_3A,General
|
||||
649,tropical,tropical,phrase,U3_5A,General
|
||||
650,"tú",you,phrase,U4_14A,General
|
||||
651,turismo,"to urism",noun,U2_3A,General
|
||||
652,turista,"to urist",phrase,U2_6A,General
|
||||
653,"turístico/a","to urist",phrase,U3_2A,General
|
||||
654,"U ubicación",location,noun,U3_2A,General
|
||||
655,"últimamente",recently,phrase,U5_4A,General
|
||||
656,"último/a",last,phrase,"U2_5A U5_3A",General
|
||||
657,"¡un abrazo!",ahug!,phrase,U5_3C,General
|
||||
658,unpoco,alittle,phrase,U1,General
|
||||
659,"único/a universidad","only university",noun,"U5_7C U1_3C",General
|
||||
660,universitario/a,"Universitystudent universe",phrase,U3_2A,General
|
||||
661,unos/as,some,phrase,U3_4A,General
|
||||
662,Uruguay,Uruguay,phrase,U0_4A,General
|
||||
663,usado/a,used,phrase,U4_2A,General
|
||||
664,usar,"to use",phrase,U4_GyC,General
|
||||
665,usted,you(formal),phrase,U1,General
|
||||
666,vacaciones,holidays,noun,U1_10B,General
|
||||
667,vainilla,vanilla,noun,U7_5A,General
|
||||
668,vale,OK,phrase,U0_6,General
|
||||
669,valer,"to beworth",verb,U9_3A,General
|
||||
670,valle,valley,noun,U3_2A,General
|
||||
671,vallenato,"Vallenato (popular",noun,U1,General
|
||||
672,vapor,"Colombianfolkmusic) steam",noun,"U2_10B U7_3A",General
|
||||
673,vaqueros,jeans,noun,U4_2A,General
|
||||
674,varios/as,several,phrase,U1,General
|
||||
675,vaso,cup,noun,U7_LEX,General
|
||||
676,vegano/a,vegan,phrase,U7_3A,General
|
||||
677,vegetal,vegetable,phrase,U7_2A,General
|
||||
678,vendedor/a,salesperson,phrase,U9_4C,General
|
||||
679,vender,"to sell",phrase,U4_1A,General
|
||||
680,venezolano/a,Venezuelan,phrase,U1_6,General
|
||||
681,"Venezuela venido/a","Venezuela comefrom",phrase,"U0_4A U8_7A",General
|
||||
682,venir,"to come",verb,U1_5A,General
|
||||
683,ventana,window,noun,U0_6,General
|
||||
684,"ver ver la televisión","to see/watch towatchtelevision",phrase,U2_2A,General
|
||||
685,verano,summer,noun,U3_8B,General
|
||||
686,"¿verdad?",right?,phrase,U3_9,General
|
||||
687,verdad,true,noun,U4_6A,General
|
||||
688,"verde verdura","green vegetable",noun,U7_2A,General
|
||||
689,vestido,dress,noun,U4_7C,General
|
||||
690,vestirse,"to getdressed",verb,U6_8A,General
|
||||
691,vez,time,noun,U2_3A,General
|
||||
692,viajar,"to travel",phrase,U2_3A,General
|
||||
693,apasionado,passionate,phrase,U5_15C,General
|
||||
694,argentino,Argentinian,phrase,U1_3A,General
|
||||
695,camarero,waiter,phrase,U1_4A,General
|
||||
696,arquitecto,architect,phrase,U1_6,General
|
||||
697,panintegral,"wholemeal bread",noun,U7_LEX,General
|
||||
698,"escribir con lápiz","to write with a pencil",noun,M1-phrase,General
|
||||
699,"Subo las escaleras.","I climb the stairs",phrase,M1-phrase,General
|
||||
700,"mundo hispano","Hispanic world",phrase,U3_7A,General
|
||||
701,hispanohablantes,"Spanish speakers",adjective,U2_9A-Extra,General
|
||||
702,"Escucho un pódcast para practicar español.","I listen to a podcast to practice Spanish",phrase,U2_9A-Extra,General
|
||||
703,"los museos de la ciudad","the city museums",phrase,U2_9-extra,General
|
||||
704,"los pódcast","the podcasts",phrase,U2_9A-Extra,General
|
||||
705,"un pódcast","a podcast",phrase,U2_9A-Extra,General
|
||||
706,"el periódico","the newspaper",noun,U1_LEX,General
|
||||
707,"cocinar platos hispanos","to cook Hispanic dishes",verb,U2_9-extra,General
|
||||
|
560
main.py
560
main.py
|
|
@ -9,6 +9,8 @@ from PyQt6.QtWidgets import (
|
|||
from PyQt6.QtCore import Qt, QUrl
|
||||
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
|
||||
from PyQt6.QtGui import QFont
|
||||
|
||||
# Internal Project Module Imports
|
||||
from database.connection import init_db, get_connection
|
||||
from core.bulk_importer import BulkImporter
|
||||
from core.clean_glossary import GlossaryCleaner
|
||||
|
|
@ -17,146 +19,226 @@ class SpanishTrainerApp(QMainWindow):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Castilian Voice Trainer Pro")
|
||||
self.setMinimumSize(1000, 650)
|
||||
self.setMinimumSize(1150, 700)
|
||||
|
||||
# 1. Initialize and Seed Database if Empty
|
||||
# 1. Initialize schema structures and check ingestion status
|
||||
self.ensure_database_populated()
|
||||
|
||||
# 2. Initialize PyQt Multimedia Audio Engine Components
|
||||
# Audio Player Architecture Setup
|
||||
self.media_player = QMediaPlayer()
|
||||
self.audio_output = QAudioOutput()
|
||||
self.media_player.setAudioOutput(self.audio_output)
|
||||
|
||||
self.current_flashcard_id = None
|
||||
|
||||
# 3. Build UI Components
|
||||
# Central Main Window Tabs Interface
|
||||
self.tabs = QTabWidget()
|
||||
self.setCentralWidget(self.tabs)
|
||||
|
||||
self.init_phrase_sandbox_tab()
|
||||
self.init_flashcard_reviewer_tab()
|
||||
|
||||
# 4. Initial Load of Database Data into Grid View
|
||||
# 2. Populate unified database rows into interface layout grid
|
||||
self.refresh_crud_table()
|
||||
|
||||
def ensure_database_populated(self):
|
||||
"""Verifies if the database exists and has records; seeds it from the PDF if empty."""
|
||||
"""Forces database configuration structure and triggers pipeline execution if empty."""
|
||||
print("🗄️ Verification Pass: Running schema configuration scripts...")
|
||||
init_db()
|
||||
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Verify if the translations table has rows populated
|
||||
try:
|
||||
cursor.execute("SELECT COUNT(*) FROM phrases")
|
||||
cursor.execute("SELECT COUNT(*) FROM translations")
|
||||
count = cursor.fetchone()[0]
|
||||
except Exception:
|
||||
print(f"📊 Current Translation Pairs found in database: {count}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Table check encountered an issue (likely empty tables): {e}")
|
||||
count = 0
|
||||
conn.close()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if count == 0:
|
||||
print("🗄️ Database appears empty. Running the structural reconstruction pipeline...")
|
||||
print("🗄️ Database tables are empty. Triggering glossary reader pipeline...")
|
||||
pdf_file = "aula_int_plus_1_glos_en_alfa.pdf"
|
||||
textbook = "Aula Internacional Plus 1"
|
||||
|
||||
if os.path.exists(pdf_file):
|
||||
# Run the layout parser staging execution pass
|
||||
importer = BulkImporter()
|
||||
importer.import_pdf_glossary(pdf_file, textbook)
|
||||
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("✨ Database successfully seeded with metadata-parsed entries.")
|
||||
print("✨ Ingestion pipeline processing sequence successfully completed.")
|
||||
else:
|
||||
print(f"⚠️ Warning: Could not find '{pdf_file}' to automatically seed records.")
|
||||
print(f"❌ Error: Source document '{pdf_file}' is missing from the directory root.")
|
||||
|
||||
# =====================================================================
|
||||
# 🗄️ TAB 1: PHRASE SANDBOX (CRUD Panel)
|
||||
# 🗄️ TAB 1: TRANSLATION-CENTRIC PHRASE SANDBOX (CRUD)
|
||||
# =====================================================================
|
||||
def init_phrase_sandbox_tab(self):
|
||||
tab = QWidget()
|
||||
layout = QHBoxLayout(tab)
|
||||
|
||||
# --- LEFT SIDE PANEL: Filter Controls & Unified Data Grid ---
|
||||
left_panel = QVBoxLayout()
|
||||
search_layout = QHBoxLayout()
|
||||
search_layout.addWidget(QLabel("🔍 Filter text:"))
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.textChanged.connect(self.refresh_crud_table)
|
||||
search_layout.addWidget(self.search_input)
|
||||
left_panel.addLayout(search_layout)
|
||||
|
||||
self.phrase_table = QTableWidget()
|
||||
self.phrase_table.setColumnCount(6)
|
||||
self.phrase_table.setHorizontalHeaderLabels(["ID", "Text", "Lang", "Type", "Voice Gender", "Deck Tag"])
|
||||
self.phrase_table.itemSelectionChanged.connect(self.handle_table_row_select)
|
||||
left_panel.addWidget(self.phrase_table)
|
||||
filter_layout = QHBoxLayout()
|
||||
filter_layout.addWidget(QLabel("🔍 Text Filter:"))
|
||||
self.search_text_input = QLineEdit()
|
||||
self.search_text_input.setPlaceholderText("Search Spanish or English text blocks...")
|
||||
self.search_text_input.textChanged.connect(self.refresh_crud_table)
|
||||
filter_layout.addWidget(self.search_text_input)
|
||||
|
||||
filter_layout.addWidget(QLabel("📂 Context:"))
|
||||
self.search_context_input = QLineEdit()
|
||||
self.search_context_input.setPlaceholderText("e.g. U2 or U8_5A")
|
||||
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([
|
||||
"TX ID", "Spanish Phrase", "English Translation", "Type", "Source Context", "Deck Assignment"
|
||||
])
|
||||
self.translation_table.itemSelectionChanged.connect(self.handle_table_row_select)
|
||||
left_panel.addWidget(self.translation_table)
|
||||
|
||||
# Row Pointer Navigation Steppers
|
||||
nav_layout = QHBoxLayout()
|
||||
self.btn_row_up = QPushButton("🔼 Previous Pair")
|
||||
self.btn_row_down = QPushButton("🔽 Next Pair")
|
||||
self.btn_row_up.clicked.connect(lambda: self.step_table_row(-1))
|
||||
self.btn_row_down.clicked.connect(lambda: self.step_table_row(1))
|
||||
nav_layout.addWidget(self.btn_row_up)
|
||||
nav_layout.addWidget(self.btn_row_down)
|
||||
left_panel.addLayout(nav_layout)
|
||||
|
||||
# --- RIGHT SIDE PANEL: Side-by-Side Unified Twin Form Box Views ---
|
||||
right_panel = QVBoxLayout()
|
||||
form_frame = QFrame()
|
||||
form_frame.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
form_layout = QFormLayout(form_frame)
|
||||
|
||||
self.input_id = QLineEdit()
|
||||
self.input_id.setReadOnly(True)
|
||||
self.input_id.setPlaceholderText("Auto-assigned ID")
|
||||
self.input_tx_id = QLineEdit()
|
||||
self.input_tx_id.setReadOnly(True)
|
||||
self.input_tx_id.setPlaceholderText("Auto-Increment ID")
|
||||
|
||||
self.input_text = QTextEdit()
|
||||
self.input_text.setMaximumHeight(60)
|
||||
self.input_text_es = QTextEdit()
|
||||
self.input_text_es.setMaximumHeight(75)
|
||||
|
||||
self.combo_lang = QComboBox()
|
||||
self.combo_lang.addItems(["es", "en"])
|
||||
self.input_text_en = QTextEdit()
|
||||
self.input_text_en.setMaximumHeight(75)
|
||||
|
||||
self.combo_type = QComboBox()
|
||||
self.combo_type.addItems(["sentence", "phrase", "noun", "verb", "adjective"])
|
||||
self.combo_type.addItems(["phrase", "sentence", "noun", "verb", "adjective"])
|
||||
|
||||
self.combo_voice_gender = QComboBox()
|
||||
self.combo_voice_gender.addItems(["female", "male"])
|
||||
|
||||
self.slider_base_speed = QSlider(Qt.Orientation.Horizontal)
|
||||
self.slider_base_speed.setMinimum(50)
|
||||
self.slider_base_speed.setMaximum(150)
|
||||
self.slider_base_speed.setValue(100)
|
||||
self.lbl_base_speed = QLabel("1.00x")
|
||||
self.slider_base_speed.valueChanged.connect(lambda v: self.lbl_base_speed.setText(f"{v/100:.2f}x"))
|
||||
|
||||
speed_box = QHBoxLayout()
|
||||
speed_box.addWidget(self.slider_base_speed)
|
||||
speed_box.addWidget(self.lbl_base_speed)
|
||||
self.input_context = QLineEdit()
|
||||
self.input_context.setPlaceholderText("e.g., U8_5A")
|
||||
|
||||
self.input_deck_tag = QLineEdit()
|
||||
self.input_deck_tag.setPlaceholderText("e.g., Anki_Unit_1")
|
||||
self.input_deck_tag.setPlaceholderText("Anki Sub-deck Hierarchy")
|
||||
|
||||
form_layout.addRow("Phrase ID Resource:", self.input_id)
|
||||
form_layout.addRow(QLabel("<b>Target Conversational Text:</b>"))
|
||||
form_layout.addRow(self.input_text)
|
||||
form_layout.addRow("Language Accent:", self.combo_lang)
|
||||
form_layout.addRow("Grammar Type Classification:", self.combo_type)
|
||||
form_layout.addRow("<b>Preferred Voice Gender:</b>", self.combo_voice_gender)
|
||||
form_layout.addRow("<b>Default Playback Speed:</b>", speed_box)
|
||||
form_layout.addRow("Deck / Group Identifier Tag:", self.input_deck_tag)
|
||||
# Common layout stylesheet for the utility buttons
|
||||
button_qss = """
|
||||
QPushButton {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #c0c0c0;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #e0e0e0;
|
||||
border: 1px solid #a0a0a0;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #d0d0d0;
|
||||
}
|
||||
"""
|
||||
|
||||
# 🔊 1. Spanish Header Layout with Speed Controls
|
||||
es_header_layout = QHBoxLayout()
|
||||
es_header_layout.setContentsMargins(0, 5, 0, 5)
|
||||
es_header_layout.addWidget(QLabel("<b>🇪🇸 Castilian Spanish Text Element:</b>"))
|
||||
|
||||
self.btn_play_sandbox_es = QPushButton("Play 🔊")
|
||||
self.btn_play_sandbox_es.setFixedWidth(75)
|
||||
self.btn_play_sandbox_es.setFixedHeight(24)
|
||||
self.btn_play_sandbox_es.setStyleSheet(button_qss)
|
||||
self.btn_play_sandbox_es.clicked.connect(self.handle_sandbox_play_es)
|
||||
es_header_layout.addWidget(self.btn_play_sandbox_es)
|
||||
|
||||
self.combo_speed_es = QComboBox()
|
||||
self.combo_speed_es.addItems(["0.50x", "0.75x", "1.00x", "1.25x", "1.50x"])
|
||||
self.combo_speed_es.setCurrentText("1.00x")
|
||||
self.combo_speed_es.setFixedWidth(70)
|
||||
self.combo_speed_es.setFixedHeight(24)
|
||||
es_header_layout.addWidget(self.combo_speed_es)
|
||||
es_header_layout.addStretch()
|
||||
|
||||
# 🔊 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("<b>🇬🇧 English Target Translation:</b>"))
|
||||
|
||||
self.btn_play_sandbox_en = QPushButton("Play 🔊")
|
||||
self.btn_play_sandbox_en.setFixedWidth(75)
|
||||
self.btn_play_sandbox_en.setFixedHeight(24)
|
||||
self.btn_play_sandbox_en.setStyleSheet(button_qss)
|
||||
self.btn_play_sandbox_en.clicked.connect(self.handle_sandbox_play_en)
|
||||
en_header_layout.addWidget(self.btn_play_sandbox_en)
|
||||
|
||||
self.combo_speed_en = QComboBox()
|
||||
self.combo_speed_en.addItems(["0.50x", "0.75x", "1.00x", "1.25x", "1.50x"])
|
||||
self.combo_speed_en.setCurrentText("1.00x")
|
||||
self.combo_speed_en.setFixedWidth(70)
|
||||
self.combo_speed_en.setFixedHeight(24)
|
||||
en_header_layout.addWidget(self.combo_speed_en)
|
||||
en_header_layout.addStretch()
|
||||
|
||||
# Mount elements sequentially into the form frame mapping structure
|
||||
form_layout.addRow("<b>Translation Link ID:</b>", self.input_tx_id)
|
||||
form_layout.addRow(es_header_layout)
|
||||
form_layout.addRow(self.input_text_es)
|
||||
form_layout.addRow(en_header_layout)
|
||||
form_layout.addRow(self.input_text_en)
|
||||
form_layout.addRow("Classification Profile:", self.combo_type)
|
||||
form_layout.addRow("Source Context ID (Raw):", self.input_context)
|
||||
form_layout.addRow("<b>Target Deck Scope:</b>", self.input_deck_tag)
|
||||
|
||||
crud_buttons = QHBoxLayout()
|
||||
self.btn_save = QPushButton("➕ Save New")
|
||||
self.btn_update = QPushButton("💾 Update Entry")
|
||||
self.btn_delete = QPushButton("🗑️ Delete")
|
||||
self.btn_save = QPushButton("➕ Create Pair")
|
||||
self.btn_update = QPushButton("💾 Update Node")
|
||||
self.btn_delete = QPushButton("🗑️ Sever Link")
|
||||
|
||||
self.btn_save.clicked.connect(self.crud_create)
|
||||
self.btn_update.clicked.connect(self.crud_update)
|
||||
self.btn_delete.clicked.connect(self.crud_delete)
|
||||
self.btn_save.clicked.connect(self.crud_create_pair)
|
||||
self.btn_update.clicked.connect(self.crud_update_pair)
|
||||
self.btn_delete.clicked.connect(self.crud_delete_pair)
|
||||
|
||||
crud_buttons.addWidget(self.btn_save)
|
||||
crud_buttons.addWidget(self.btn_update)
|
||||
crud_buttons.addWidget(self.btn_delete)
|
||||
|
||||
right_panel.addWidget(QLabel("<h3>Configure Phrase & Voice Variables</h3>"))
|
||||
right_panel.addWidget(QLabel("<h3>Translation Node Management Matrix</h3>"))
|
||||
right_panel.addWidget(form_frame)
|
||||
right_panel.addLayout(crud_buttons)
|
||||
right_panel.addStretch()
|
||||
|
||||
layout.addLayout(left_panel, stretch=3)
|
||||
layout.addLayout(right_panel, stretch=2)
|
||||
layout.addLayout(left_panel, stretch=4)
|
||||
layout.addLayout(right_panel, stretch=3)
|
||||
|
||||
self.tabs.addTab(tab, "🗄️ Phrase Sandbox (CRUD)")
|
||||
|
||||
# =====================================================================
|
||||
# 🃏 TAB 2: FLASHCARD STUDY PLAYER
|
||||
# 🃏 TAB 2: FLASHCARD STUDY MODULE
|
||||
# =====================================================================
|
||||
def init_flashcard_reviewer_tab(self):
|
||||
tab = QWidget()
|
||||
|
|
@ -166,11 +248,11 @@ class SpanishTrainerApp(QMainWindow):
|
|||
card_frame.setStyleSheet("background-color: #ffffff; border: 2px solid #bdc3c7; border-radius: 12px;")
|
||||
card_layout = QVBoxLayout(card_frame)
|
||||
|
||||
self.lbl_card_text = QLabel("Click 'Load Next' to study sentences...")
|
||||
self.lbl_card_text = QLabel("Select 'Next Card' to initiate study sequence...")
|
||||
self.lbl_card_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.lbl_card_text.setFont(QFont("Arial", 22, QFont.Weight.Bold))
|
||||
self.lbl_card_text.setWordWrap(True)
|
||||
self.lbl_card_text.setStyleSheet("color: #2c3e50; border: none; padding: 20px;")
|
||||
self.lbl_card_text.setStyleSheet("color: #2c3e50; border: none; padding: 25px;")
|
||||
|
||||
self.lbl_card_meta = QLabel("")
|
||||
self.lbl_card_meta.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
|
@ -183,22 +265,19 @@ class SpanishTrainerApp(QMainWindow):
|
|||
card_layout.addStretch()
|
||||
|
||||
playback_layout = QHBoxLayout()
|
||||
playback_layout.addWidget(QLabel("🔊 Fine-Tune Study Speed:"))
|
||||
|
||||
playback_layout.addWidget(QLabel("🔊 Voice Track Speed:"))
|
||||
self.slider_review_speed = QSlider(Qt.Orientation.Horizontal)
|
||||
self.slider_review_speed.setMinimum(50)
|
||||
self.slider_review_speed.setMaximum(150)
|
||||
self.slider_review_speed.setValue(100)
|
||||
self.lbl_review_speed = QLabel("1.0x (Normal)")
|
||||
|
||||
self.lbl_review_speed = QLabel("1.0x")
|
||||
self.slider_review_speed.valueChanged.connect(self.handle_live_speed_change)
|
||||
|
||||
playback_layout.addWidget(self.slider_review_speed)
|
||||
playback_layout.addWidget(self.lbl_review_speed)
|
||||
|
||||
action_buttons = QHBoxLayout()
|
||||
self.btn_play_voice = QPushButton("🗣️ Play Voice Track")
|
||||
self.btn_flip_card = QPushButton("👁️ Reveal Translation")
|
||||
self.btn_flip_card = QPushButton("👁️ Reveal English Partner")
|
||||
self.btn_load_next = QPushButton("➡️ Next Card")
|
||||
|
||||
self.btn_play_voice.clicked.connect(self.handle_play_voice)
|
||||
|
|
@ -214,171 +293,258 @@ class SpanishTrainerApp(QMainWindow):
|
|||
layout.addLayout(playback_layout)
|
||||
layout.addLayout(action_buttons)
|
||||
|
||||
self.tabs.addTab(tab, "🃏 Flashcard Study")
|
||||
self.tabs.addTab(tab, "🃏 Flashcard Review")
|
||||
|
||||
# =====================================================================
|
||||
# ⚡ ENGINE BUSINESS LOGIC OPERATIONS & DATABASE MAPPINGS
|
||||
# ⚡ ENGINE DATABASE LOGIC & FILTER COMPILATIONS
|
||||
# =====================================================================
|
||||
|
||||
def refresh_crud_table(self):
|
||||
"""Pulls unified translation nodes into pairs while enforcing context text matches."""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
filter_text = self.search_input.text()
|
||||
if filter_text:
|
||||
cursor.execute("""
|
||||
SELECT id, text, language, word_type, voice_gender, deck_name
|
||||
FROM phrases WHERE text LIKE ? ORDER BY id DESC LIMIT 100
|
||||
""", (f"%{filter_text}%",))
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT id, text, language, word_type, voice_gender, deck_name
|
||||
FROM phrases ORDER BY id DESC LIMIT 100
|
||||
""")
|
||||
text_filter = self.search_text_input.text().strip()
|
||||
context_filter = self.search_context_input.text().strip()
|
||||
|
||||
query = """
|
||||
SELECT t.translation_id, p1.text, p2.text, p1.word_type, p1.source_context, t.deck_name
|
||||
FROM translations t
|
||||
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
||||
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
||||
WHERE p1.language = 'es' AND p2.language = 'en'
|
||||
"""
|
||||
params = []
|
||||
|
||||
if text_filter:
|
||||
query += " AND (p1.text LIKE ? OR p2.text LIKE ?)"
|
||||
params.extend([f"%{text_filter}%", f"%{text_filter}%"])
|
||||
|
||||
if context_filter:
|
||||
query += " AND p1.source_context LIKE ?"
|
||||
params.append(f"%{context_filter}%")
|
||||
|
||||
query += " ORDER BY t.translation_id ASC LIMIT 250"
|
||||
|
||||
cursor.execute(query, params)
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
self.phrase_table.setRowCount(0)
|
||||
self.translation_table.setRowCount(0)
|
||||
for row_idx, row_data in enumerate(rows):
|
||||
self.phrase_table.insertRow(row_idx)
|
||||
for col_idx, value in enumerate(row_data):
|
||||
self.phrase_table.setItem(row_idx, col_idx, QTableWidgetItem(str(value if value is not None else "")))
|
||||
self.translation_table.insertRow(row_idx)
|
||||
for col_idx in range(6):
|
||||
val = row_data[col_idx]
|
||||
self.translation_table.setItem(row_idx, col_idx, QTableWidgetItem(str(val if val is not None else "")))
|
||||
|
||||
def handle_table_row_select(self):
|
||||
selected_ranges = self.phrase_table.selectedRanges()
|
||||
selected_ranges = self.translation_table.selectedRanges()
|
||||
if not selected_ranges:
|
||||
return
|
||||
row = selected_ranges[0].topRow()
|
||||
item = self.phrase_table.item(row, 0)
|
||||
if not item:
|
||||
tx_id_item = self.translation_table.item(row, 0)
|
||||
if not tx_id_item:
|
||||
return
|
||||
phrase_id = item.text()
|
||||
|
||||
tx_id = tx_id_item.text()
|
||||
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT id, text, language, word_type, voice_gender, base_speed, deck_name FROM phrases WHERE id = ?", (phrase_id,))
|
||||
cursor.execute("""
|
||||
SELECT t.translation_id, p1.text, p2.text, p1.word_type, p1.source_context, t.deck_name
|
||||
FROM translations t
|
||||
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
||||
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
||||
WHERE t.translation_id = ?
|
||||
""", (tx_id,))
|
||||
record = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if record:
|
||||
self.input_id.setText(str(record[0]))
|
||||
self.input_text.setPlainText(str(record[1]))
|
||||
self.combo_lang.setCurrentText(str(record[2]))
|
||||
self.combo_type.setCurrentText(str(record[3]) if record[3] else "sentence")
|
||||
self.combo_voice_gender.setCurrentText(str(record[4]) if record[4] else "female")
|
||||
self.input_tx_id.setText(str(record[0]))
|
||||
self.input_text_es.setPlainText(str(record[1]))
|
||||
self.input_text_en.setPlainText(str(record[2]))
|
||||
self.combo_type.setCurrentText(str(record[3]) if record[3] else "phrase")
|
||||
self.input_context.setText(str(record[4]) if record[4] else "")
|
||||
self.input_deck_tag.setText(str(record[5]) if record[5] else "General")
|
||||
|
||||
speed_val = int((record[5] if record[5] else 1.0) * 100)
|
||||
self.slider_base_speed.setValue(speed_val)
|
||||
self.input_deck_tag.setText(str(record[6]) if record[6] else "General")
|
||||
def 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)
|
||||
|
||||
def crud_create(self):
|
||||
def crud_create_pair(self):
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Write Spanish node entry
|
||||
cursor.execute("""
|
||||
INSERT INTO phrases (text, language, word_type, voice_gender, base_speed, deck_name)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
self.input_text.toPlainText().strip(),
|
||||
self.combo_lang.currentText(),
|
||||
self.combo_type.currentText(),
|
||||
self.combo_voice_gender.currentText(),
|
||||
self.slider_base_speed.value() / 100.0, # Fixed typo: changed from .setValue() to .value()
|
||||
self.input_deck_tag.text().strip() or "General"
|
||||
))
|
||||
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
|
||||
cursor.execute("""
|
||||
INSERT INTO translations (source_phrase_id, target_phrase_id, deck_name) VALUES (?, ?, ?)
|
||||
""", (es_id, en_id, self.input_deck_tag.text().strip() or "General"))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
self.refresh_crud_table()
|
||||
QMessageBox.information(self, "Success", "Phrase generated into database index store successfully.")
|
||||
QMessageBox.information(self, "Success", "Isolated phrase pairs created and relational link bound.")
|
||||
|
||||
def crud_update(self):
|
||||
pid = self.input_id.text()
|
||||
if not pid:
|
||||
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
|
||||
# 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=?",
|
||||
(self.input_text_en.toPlainText().strip(), self.combo_type.currentText(), self.input_context.text().strip(), en_id))
|
||||
cursor.execute("UPDATE translations SET deck_name=? WHERE translation_id=?",
|
||||
(self.input_deck_tag.text().strip() or "General", tx_id))
|
||||
conn.commit()
|
||||
|
||||
conn.close()
|
||||
self.refresh_crud_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("""
|
||||
UPDATE phrases SET text=?, language=?, word_type=?, voice_gender=?, base_speed=?, deck_name=?
|
||||
WHERE id=?
|
||||
""", (
|
||||
self.input_text.toPlainText().strip(),
|
||||
self.combo_lang.currentText(),
|
||||
self.combo_type.currentText(),
|
||||
self.combo_voice_gender.currentText(),
|
||||
self.slider_base_speed.value() / 100.0,
|
||||
self.input_deck_tag.text().strip() or "General",
|
||||
pid
|
||||
))
|
||||
conn.commit()
|
||||
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()
|
||||
QMessageBox.information(self, "Success", "Database record fields updated cleanly.")
|
||||
self.input_tx_id.clear()
|
||||
self.input_text_es.clear()
|
||||
self.input_text_en.clear()
|
||||
|
||||
def crud_delete(self):
|
||||
pid = self.input_id.text()
|
||||
if not pid:
|
||||
# =====================================================================
|
||||
# 🔊 AUDIO OPERATIONS & FLASHCARD CONTROL
|
||||
# =====================================================================
|
||||
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
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM phrases WHERE id=?", (pid,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
self.input_id.clear()
|
||||
self.input_text.clear()
|
||||
self.refresh_crud_table()
|
||||
|
||||
# =====================================================================
|
||||
# 🔊 AUDIO PLAYER SYSTEM
|
||||
# =====================================================================
|
||||
safe_name = "".join([c for c in text_str if c.isalnum() or c in (" ", "_")]).rstrip().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 (" ", "_")]).rstrip().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."""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT id, text, language, word_type, voice_gender, base_speed FROM phrases ORDER BY RANDOM() LIMIT 1")
|
||||
cursor.execute("""
|
||||
SELECT t.translation_id, p1.text, p1.word_type, p1.source_context, t.deck_name, p1.id
|
||||
FROM translations t
|
||||
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
||||
WHERE p1.language = 'es'
|
||||
ORDER BY RANDOM() LIMIT 1
|
||||
""")
|
||||
record = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if record:
|
||||
self.current_flashcard_id = record[0]
|
||||
self.current_flashcard_id = record[5]
|
||||
self.lbl_card_text.setText(record[1])
|
||||
|
||||
lang_lbl = "Spanish Accent" if record[2] == "es" else "English Accent"
|
||||
gender_lbl = str(record[4]).capitalize() if record[4] else "Female"
|
||||
self.lbl_card_meta.setText(f"Classification: {record[3]} • Configured Voice: {lang_lbl} ({gender_lbl})")
|
||||
|
||||
card_saved_speed = int((record[5] if record[5] else 1.0) * 100)
|
||||
self.slider_review_speed.setValue(card_saved_speed)
|
||||
self.lbl_card_meta.setText(f"Tx Link node: {record[0]} • Context: {record[3]} • Subdeck: {record[4]}")
|
||||
|
||||
def handle_play_voice(self):
|
||||
if not self.current_flashcard_id:
|
||||
return
|
||||
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT text, language, voice_gender FROM phrases WHERE id = ?", (self.current_flashcard_id,))
|
||||
phrase_row = cursor.fetchone()
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not phrase_row:
|
||||
return
|
||||
if row:
|
||||
text_str, lang, gender = row
|
||||
safe_name = "".join([c for c in text_str if c.isalnum() or c in (" ", "_")]).rstrip().replace(" ", "_").lower()
|
||||
|
||||
text_str, lang_code, voice_gender = phrase_row
|
||||
resolved_gender = gender if gender else "female"
|
||||
target_file = f"media/{safe_name}_{lang}_{resolved_gender}.mp3"
|
||||
|
||||
safe_filename = "".join([c for c in text_str if c.isalnum() or c in (" ", "_")]).rstrip().replace(" ", "_").lower()
|
||||
target_audio_file = f"media/{safe_filename}_{lang_code}_{voice_gender}.mp3"
|
||||
|
||||
os.makedirs("media", exist_ok=True)
|
||||
|
||||
if not os.path.exists(target_audio_file):
|
||||
QMessageBox.warning(self, "Audio Track Missing",
|
||||
f"Audio track asset file not found in directory:\n'{target_audio_file}'\n\nRun the background Edge-TTS batch generator script next to download this track automatically.")
|
||||
return
|
||||
|
||||
self.media_player.setSource(QUrl.fromLocalFile(os.path.abspath(target_audio_file)))
|
||||
|
||||
current_rate = self.slider_review_speed.value() / 100.0
|
||||
self.media_player.setPlaybackRate(current_rate)
|
||||
|
||||
self.media_player.play()
|
||||
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
|
||||
|
|
@ -393,18 +559,40 @@ class SpanishTrainerApp(QMainWindow):
|
|||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT 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
|
||||
WHERE t.source_phrase_id = ?
|
||||
WHERE p1.id = ?
|
||||
""", (self.current_flashcard_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if row:
|
||||
current_es = self.lbl_card_text.text().split("\n\n👉")[0]
|
||||
self.lbl_card_text.setText(f"{current_es}\n\n👉 [ {row[0]} ]")
|
||||
clean_es = self.lbl_card_text.text().split("\n\n👉")[0]
|
||||
self.lbl_card_text.setText(f"{clean_es}\n\n👉 [ {row[0]} ]")
|
||||
|
||||
# =====================================================================
|
||||
# 🚀 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)
|
||||
|
||||
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)
|
||||
|
|
|
|||
2153
spanish_trainer_backup.sql
Normal file
2153
spanish_trainer_backup.sql
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue