# anki_exporter.py import os import tempfile import time import genanki import database from tts_utils import parse_text_for_edgetts from kokoro_utils import generate_local_speech def compile_anki_package(records, output_path, deck_name): """ Compiles database records into a bidirectional card payload package. Resolves voice models dynamically by gender selection parameters and applies speed multipliers from active configurations using local Kokoro TTS. Injects sort_order as field 0 for structured card sequencing. """ # Generate deterministic positive 32-bit integers from deck name and model name deck_id = abs(hash(deck_name)) % (2**31) model_id = abs(hash("Spanish Bidirectional Multi-Note HTML Model")) % (2**31) # Unique timestamp prefix for media files to prevent overwriting prior exports in Anki run_prefix = int(time.time()) # Global Configuration Pace Resolver Mapping settings = database.load_all_settings() or {} try: speed_multiplier = float(settings.get("tts_playback_speed", 1.0)) except (TypeError, ValueError): speed_multiplier = 1.0 anki_model = genanki.Model( model_id, 'Spanish Bidirectional Multi-Note HTML Model', fields=[ {'name': 'SortOrder'}, # Field 0: Anki primary sort field {'name': 'EnglishText'}, {'name': 'SpanishText'}, {'name': 'AnkiNotes'}, {'name': 'EnglishAudio'}, {'name': 'SpanishAudio'} ], templates=[ { 'name': 'Card 1: English ➔ Spanish', 'qfmt': '
TRANSLATE TO SPANISH:

' '
{{EnglishText}}

' '
{{EnglishAudio}}
', 'afmt': '{{FrontSide}}
' '
{{SpanishText}}

' '{{#AnkiNotes}}
Anki Meta: {{{AnkiNotes}}}
{{/AnkiNotes}}
' '
{{SpanishAudio}}
', }, { 'name': 'Card 2: Spanish ➔ English', 'qfmt': '
TRANSLATE TO ENGLISH:

' '
{{SpanishText}}

' '
{{SpanishAudio}}
', 'afmt': '{{FrontSide}}
' '
{{EnglishText}}

' '{{#AnkiNotes}}
Anki Meta: {{{AnkiNotes}}}
{{/AnkiNotes}}
' '
{{EnglishAudio}}
', } ] ) deck = genanki.Deck(deck_id, deck_name) media_files_to_pack = [] with tempfile.TemporaryDirectory() as tmpdir: for idx, record in enumerate(records): en_raw = record["en_text"] es_raw = record["es_text"] gender_flag = record.get("gender", "Female") # Read sort_order from database record (fallback to loop index if unset) raw_order = record.get("sort_order") order_val = int(raw_order) if raw_order is not None else (idx + 1) sort_order_str = f"{order_val:04d}" # Zero-padded integer string for exact sorting # Sanitize text payloads for TTS engine en_tts_text = parse_text_for_edgetts(en_raw) es_tts_text = parse_text_for_edgetts(es_raw) # Map Kokoro voices matching gender settings spanish_voice = "em_alex" if gender_flag == "Male" else "ef_dora" english_voice = "af_bella" raw_tags = record.get("tags") or "" note_tags = [t.strip().replace(" ", "_") for t in raw_tags.split(",") if t.strip()] anki_notes_html = record['anki_notes'].strip() if record.get('anki_notes') else "" # Unique Media Filenames combining execution timestamp and index (.wav output from soundfile) en_audio_filename = f"kokoro_en_{run_prefix}_{idx}.wav" es_audio_filename = f"kokoro_es_{run_prefix}_{idx}.wav" en_audio_path = os.path.join(tmpdir, en_audio_filename) es_audio_path = os.path.join(tmpdir, es_audio_filename) # English Audio Synthesis via Kokoro try: if en_tts_text.strip(): generated_path = generate_local_speech(en_tts_text, voice_name=english_voice, speed=speed_multiplier) if os.path.exists(generated_path): # Copy temp file to designated exporter temp directory path import shutil shutil.copyfile(generated_path, en_audio_path) media_files_to_pack.append(en_audio_path) en_audio_field = f"[sound:{en_audio_filename}]" else: en_audio_field = "" else: en_audio_field = "" except Exception as e: print(f"[Anki Exporter] English TTS error for card {idx}: {e}") en_audio_field = "" # Spanish Audio Synthesis via Kokoro try: if es_tts_text.strip(): generated_path = generate_local_speech(es_tts_text, voice_name=spanish_voice, speed=speed_multiplier) if os.path.exists(generated_path): import shutil shutil.copyfile(generated_path, es_audio_path) media_files_to_pack.append(es_audio_path) es_audio_field = f"[sound:{es_audio_filename}]" else: es_audio_field = "" else: es_audio_field = "" except Exception as e: print(f"[Anki Exporter] Spanish TTS error for card {idx}: {e}") es_audio_field = "" note = genanki.Note( model=anki_model, fields=[sort_order_str, en_raw, es_raw, anki_notes_html, en_audio_field, es_audio_field], tags=note_tags ) deck.add_note(note) package = genanki.Package(deck) package.media_files = media_files_to_pack package.write_to_file(output_path)