diff --git a/main.py b/main.py index 18b9bf3..8dc4bf6 100644 --- a/main.py +++ b/main.py @@ -778,33 +778,42 @@ class SpanishTrainerApp(QMainWindow): 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): - """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: QMessageBox.warning(self, "Export Cancelled", "The current study stack is empty. Verify your search filters.") return # 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) - # Standardized Castilian Note Template Structure Definition + # Standardized Castilian Note Template Structure Definition (Includes English Audio) spanish_note_model = genanki.Model( model_id, - 'Castilian Audio Flashcard Model', + 'Castilian Audio Flashcard Model v2', fields=[ {'name': 'SpanishPhrase'}, {'name': 'EnglishTranslation'}, {'name': 'GrammarNotes'}, - {'name': 'AudioTrack'} + {'name': 'SpanishAudio'}, + {'name': 'EnglishAudio'} ], templates=[ { 'name': 'Card 1: Auditory Identification', - 'qfmt': '
{{SpanishPhrase}}

{{AudioTrack}}
', - 'afmt': '{{FrontSide}}
{{EnglishTranslation}}

{{GrammarNotes}}
', + 'qfmt': ( + '
{{SpanishPhrase}}
' + '
{{SpanishAudio}}
' + ), + 'afmt': ( + '{{FrontSide}}
' + '
{{EnglishTranslation}}
' + '
{{EnglishAudio}}

' + '
{{GrammarNotes}}
' + ), }, ], 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() 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 = {} 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 conn = get_connection() @@ -838,7 +847,7 @@ class SpanishTrainerApp(QMainWindow): placeholders = ",".join(["?"] * len(self.flashcard_ids_pool)) 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 JOIN phrases p1 ON t.source_phrase_id = p1.id JOIN phrases p2 ON t.target_phrase_id = p2.id @@ -855,20 +864,32 @@ class SpanishTrainerApp(QMainWindow): en_text = row[2].strip() notes_text = row[3].strip() if row[3] else "" tags_string = row[4].strip() if row[4] else "" - lang = row[5] - # Reconstruct local media naming signature - safe_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" - filename_only = f"{safe_audio_name}_{lang}_female.mp3" + # --- Handle Spanish Audio Mapping --- + safe_es_audio_name = "".join([c for c in es_text if c.isalnum() or c in (" ", "_")]).strip().replace(" ", "_").lower() + relative_es_path = f"media/{safe_es_audio_name}_es_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_audio_path): - media_files_manifest.append(relative_audio_path) - anki_audio_field = f"[sound:{filename_only}]" + if os.path.exists(relative_es_path): + if relative_es_path not in media_files_manifest: + media_files_manifest.append(relative_es_path) + anki_es_audio_field = f"[sound:{es_filename_only}]" else: - missing_assets_count += 1 - anki_audio_field = "" # Fallback to empty if not generated yet + missing_es_audio += 1 + 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 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 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( 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 ) decks_map[db_deck_name].add_note(flash_note) @@ -893,9 +914,17 @@ class SpanishTrainerApp(QMainWindow): package.media_files = media_files_manifest 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)}" - if missing_assets_count > 0: - 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." + success_msg = ( + f"✨ Packaging complete!\n\n" + 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)