exports both Spanish and English audio to Anki
This commit is contained in:
parent
bed5be9997
commit
9a295c2c90
1 changed files with 56 additions and 27 deletions
83
main.py
83
main.py
|
|
@ -778,33 +778,42 @@ class SpanishTrainerApp(QMainWindow):
|
||||||
self.media_player.setPlaybackRate(rate)
|
self.media_player.setPlaybackRate(rate)
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
# 📦 ARTIFACT EXPORT GATEWAYS (GENANKI LIVE ENGINE)
|
# 📦 ARTIFACT EXPORT GATEWAYS (GENANKI LIVE ENGINE WITH DUAL AUDIO)
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
def handle_export_anki_deck(self):
|
def handle_export_anki_deck(self):
|
||||||
"""Compiles active subset into functional .apkg with bundled neural audio tracks."""
|
"""Compiles active subset into functional .apkg with bundled Spanish and English audio tracks."""
|
||||||
if not self.flashcard_ids_pool:
|
if not self.flashcard_ids_pool:
|
||||||
QMessageBox.warning(self, "Export Cancelled", "The current study stack is empty. Verify your search filters.")
|
QMessageBox.warning(self, "Export Cancelled", "The current study stack is empty. Verify your search filters.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 1. Generate Stable Cryptographic Note Model ID
|
# 1. Generate Stable Cryptographic Note Model ID
|
||||||
model_hash = hashlib.sha256(b"castilian_voice_trainer_model_v1").hexdigest()
|
model_hash = hashlib.sha256(b"castilian_voice_trainer_model_v2").hexdigest()
|
||||||
model_id = int(model_hash[:13], 16)
|
model_id = int(model_hash[:13], 16)
|
||||||
|
|
||||||
# Standardized Castilian Note Template Structure Definition
|
# Standardized Castilian Note Template Structure Definition (Includes English Audio)
|
||||||
spanish_note_model = genanki.Model(
|
spanish_note_model = genanki.Model(
|
||||||
model_id,
|
model_id,
|
||||||
'Castilian Audio Flashcard Model',
|
'Castilian Audio Flashcard Model v2',
|
||||||
fields=[
|
fields=[
|
||||||
{'name': 'SpanishPhrase'},
|
{'name': 'SpanishPhrase'},
|
||||||
{'name': 'EnglishTranslation'},
|
{'name': 'EnglishTranslation'},
|
||||||
{'name': 'GrammarNotes'},
|
{'name': 'GrammarNotes'},
|
||||||
{'name': 'AudioTrack'}
|
{'name': 'SpanishAudio'},
|
||||||
|
{'name': 'EnglishAudio'}
|
||||||
],
|
],
|
||||||
templates=[
|
templates=[
|
||||||
{
|
{
|
||||||
'name': 'Card 1: Auditory Identification',
|
'name': 'Card 1: Auditory Identification',
|
||||||
'qfmt': '<div style="font-family: Arial; font-size: 24px; text-align: center; color: #2c3e50;">{{SpanishPhrase}}</div><br><div style="text-align: center;">{{AudioTrack}}</div>',
|
'qfmt': (
|
||||||
'afmt': '{{FrontSide}}<hr id="answer"><div style="font-family: Arial; font-size: 20px; text-align: center; color: #27ae60; font-weight: bold;">{{EnglishTranslation}}</div><br><div style="font-family: Arial; font-size: 14px; text-align: center; color: #7f8c8d; font-style: italic;">{{GrammarNotes}}</div>',
|
'<div style="font-family: Arial; font-size: 24px; text-align: center; color: #2c3e50;">{{SpanishPhrase}}</div>'
|
||||||
|
'<br><div style="text-align: center;">{{SpanishAudio}}</div>'
|
||||||
|
),
|
||||||
|
'afmt': (
|
||||||
|
'{{FrontSide}}<hr id="answer">'
|
||||||
|
'<div style="font-family: Arial; font-size: 20px; text-align: center; color: #27ae60; font-weight: bold;">{{EnglishTranslation}}</div>'
|
||||||
|
'<div style="text-align: center; margin-top: 5px;">{{EnglishAudio}}</div><br>'
|
||||||
|
'<div style="font-family: Arial; font-size: 14px; text-align: center; color: #7f8c8d; font-style: italic;">{{GrammarNotes}}</div>'
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
css='.card { font-family: arial; font-size: 20px; text-align: center; background-color: #f8f9fa; }'
|
css='.card { font-family: arial; font-size: 20px; text-align: center; background-color: #f8f9fa; }'
|
||||||
|
|
@ -827,10 +836,10 @@ class SpanishTrainerApp(QMainWindow):
|
||||||
file_title = "".join([c for c in file_title if c.isalnum() or c in (".", "_", "-")]).strip()
|
file_title = "".join([c for c in file_title if c.isalnum() or c in (".", "_", "-")]).strip()
|
||||||
destination_path = os.path.join(self.anki_export_dir, file_title)
|
destination_path = os.path.join(self.anki_export_dir, file_title)
|
||||||
|
|
||||||
# Dictionary to store separate deck objects dynamically based on the DB assignments
|
|
||||||
decks_map = {}
|
decks_map = {}
|
||||||
media_files_manifest = []
|
media_files_manifest = []
|
||||||
missing_assets_count = 0
|
missing_es_audio = 0
|
||||||
|
missing_en_audio = 0
|
||||||
|
|
||||||
# 2. Fetch specific database rows matching the current runtime array pool
|
# 2. Fetch specific database rows matching the current runtime array pool
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
|
|
@ -838,7 +847,7 @@ class SpanishTrainerApp(QMainWindow):
|
||||||
|
|
||||||
placeholders = ",".join(["?"] * len(self.flashcard_ids_pool))
|
placeholders = ",".join(["?"] * len(self.flashcard_ids_pool))
|
||||||
query = f"""
|
query = f"""
|
||||||
SELECT t.deck_name, p1.text, p2.text, t.notes, t.tags, p1.language
|
SELECT t.deck_name, p1.text, p2.text, t.notes, t.tags
|
||||||
FROM translations t
|
FROM translations t
|
||||||
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
JOIN phrases p1 ON t.source_phrase_id = p1.id
|
||||||
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
JOIN phrases p2 ON t.target_phrase_id = p2.id
|
||||||
|
|
@ -855,20 +864,32 @@ class SpanishTrainerApp(QMainWindow):
|
||||||
en_text = row[2].strip()
|
en_text = row[2].strip()
|
||||||
notes_text = row[3].strip() if row[3] else ""
|
notes_text = row[3].strip() if row[3] else ""
|
||||||
tags_string = row[4].strip() if row[4] else ""
|
tags_string = row[4].strip() if row[4] else ""
|
||||||
lang = row[5]
|
|
||||||
|
|
||||||
# Reconstruct local media naming signature
|
# --- Handle Spanish Audio Mapping ---
|
||||||
safe_audio_name = "".join([c for c in es_text if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
safe_es_audio_name = "".join([c for c in es_text if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
||||||
relative_audio_path = f"media/{safe_audio_name}_{lang}_female.mp3"
|
relative_es_path = f"media/{safe_es_audio_name}_es_female.mp3"
|
||||||
filename_only = f"{safe_audio_name}_{lang}_female.mp3"
|
es_filename_only = f"{safe_es_audio_name}_es_female.mp3"
|
||||||
|
|
||||||
# Check if target file exists. If missing, flag but don't crash
|
if os.path.exists(relative_es_path):
|
||||||
if os.path.exists(relative_audio_path):
|
if relative_es_path not in media_files_manifest:
|
||||||
media_files_manifest.append(relative_audio_path)
|
media_files_manifest.append(relative_es_path)
|
||||||
anki_audio_field = f"[sound:{filename_only}]"
|
anki_es_audio_field = f"[sound:{es_filename_only}]"
|
||||||
else:
|
else:
|
||||||
missing_assets_count += 1
|
missing_es_audio += 1
|
||||||
anki_audio_field = "" # Fallback to empty if not generated yet
|
anki_es_audio_field = ""
|
||||||
|
|
||||||
|
# --- Handle English Audio Mapping ---
|
||||||
|
safe_en_audio_name = "".join([c for c in en_text if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower()
|
||||||
|
relative_en_path = f"media/{safe_en_audio_name}_en_female.mp3"
|
||||||
|
en_filename_only = f"{safe_en_audio_name}_en_female.mp3"
|
||||||
|
|
||||||
|
if os.path.exists(relative_en_path):
|
||||||
|
if relative_en_path not in media_files_manifest:
|
||||||
|
media_files_manifest.append(relative_en_path)
|
||||||
|
anki_en_audio_field = f"[sound:{en_filename_only}]"
|
||||||
|
else:
|
||||||
|
missing_en_audio += 1
|
||||||
|
anki_en_audio_field = ""
|
||||||
|
|
||||||
# Handle dynamic deck assignment structure
|
# Handle dynamic deck assignment structure
|
||||||
if db_deck_name not in decks_map:
|
if db_deck_name not in decks_map:
|
||||||
|
|
@ -879,10 +900,10 @@ class SpanishTrainerApp(QMainWindow):
|
||||||
# Split tags string by spaces into an array list for genanki
|
# Split tags string by spaces into an array list for genanki
|
||||||
parsed_tags = [t for t in tags_string.replace(",", " ").split(" ") if t]
|
parsed_tags = [t for t in tags_string.replace(",", " ").split(" ") if t]
|
||||||
|
|
||||||
# Construct unique note entry template matching properties
|
# Construct unique note entry template matching updated fields array
|
||||||
flash_note = genanki.Note(
|
flash_note = genanki.Note(
|
||||||
model=spanish_note_model,
|
model=spanish_note_model,
|
||||||
fields=[es_text, en_text, notes_text, anki_audio_field],
|
fields=[es_text, en_text, notes_text, anki_es_audio_field, anki_en_audio_field],
|
||||||
tags=parsed_tags
|
tags=parsed_tags
|
||||||
)
|
)
|
||||||
decks_map[db_deck_name].add_note(flash_note)
|
decks_map[db_deck_name].add_note(flash_note)
|
||||||
|
|
@ -893,9 +914,17 @@ class SpanishTrainerApp(QMainWindow):
|
||||||
package.media_files = media_files_manifest
|
package.media_files = media_files_manifest
|
||||||
package.write_to_file(destination_path)
|
package.write_to_file(destination_path)
|
||||||
|
|
||||||
success_msg = f"✨ Packaged complete!\n\nFile Output: {file_title}\nDestination: {self.anki_export_dir}\nTotal Cards Built: {len(records)}\nDecks Created: {len(decks_map)}"
|
success_msg = (
|
||||||
if missing_assets_count > 0:
|
f"✨ Packaging complete!\n\n"
|
||||||
success_msg += f"\n\n⚠️ Note: {missing_assets_count} cards were bundled without audio tracks because their voice files hadn't been generated in the review window yet."
|
f"File Output: {file_title}\n"
|
||||||
|
f"Destination: {self.anki_export_dir}\n"
|
||||||
|
f"Total Cards Built: {len(records)}\n"
|
||||||
|
f"Decks Created: {len(decks_map)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if missing_es_audio > 0 or missing_en_audio > 0:
|
||||||
|
success_msg += f"\n\n⚠️ Note: Missing files detected (Spanish: {missing_es_audio}, English: {missing_en_audio}). " \
|
||||||
|
f"Cards were bundled without corresponding audio fields if they hadn't been triggered in the review window yet."
|
||||||
|
|
||||||
QMessageBox.information(self, "Export Complete", success_msg)
|
QMessageBox.information(self, "Export Complete", success_msg)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue