# anki_exporter.py
import os
import tempfile
import asyncio
import genanki
import edge_tts
import shutil
import database
async def generate_edge_audio(text, voice, output_path, rate_modifier="+0%"):
"""Asynchronously streams data packages via the Microsoft Edge API pipeline."""
try:
communicate = edge_tts.Communicate(text=text, voice=voice, rate=rate_modifier)
await communicate.save(output_path)
return True
except Exception as e:
print(f"Edge-TTS synthesis anomaly: {e}")
return False
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
global speed coefficient rates from the active configurations.
"""
# Bumping IDs to completely clear any remaining local cache
model_id = 1684329050
deck_id = 1684329050
# Global Configuration Pace Resolver Mapping
settings = database.load_all_settings() or {}
config_speed = settings.get("tts_playback_speed", "1.0")
# Transform numeric string floats (e.g., 1.2) into Edge-TTS percentage strings (e.g., +20%)
try:
pct = int((float(config_speed) - 1.0) * 100)
rate_string = f"{'+' if pct >= 0 else ''}{pct}%"
except Exception:
rate_string = "+0%"
anki_model = genanki.Model(
model_id,
'Spanish Bidirectional Multi-Note HTML Model',
fields=[
{'name': 'EnglishText'},
{'name': 'SpanishText'},
{'name': 'Notes'},
{'name': 'AnkiNotes'},
{'name': 'EnglishAudio'},
{'name': 'SpanishAudio'}
],
templates=[
{
'name': 'Card 1: English ➔ Spanish',
'qfmt': '
TRANSLATE TO SPANISH:
'
'{{EnglishText}}
'
'{{EnglishAudio}}
',
'afmt': '{{FrontSide}}
'
'{{SpanishText}}
'
'{{Notes}}
'
'{{#AnkiNotes}}Anki Meta: {{{AnkiNotes}}}
{{/AnkiNotes}}
'
'{{SpanishAudio}}
',
},
{
'name': 'Card 2: Spanish ➔ English',
'qfmt': 'TRANSLATE TO ENGLISH:
'
'{{SpanishText}}
'
'{{SpanishAudio}}
',
'afmt': '{{FrontSide}}
'
'{{EnglishText}}
'
'{{Notes}}
'
'{{#AnkiNotes}}Anki Meta: {{{AnkiNotes}}}
{{/AnkiNotes}}
'
'{{EnglishAudio}}
',
}
]
)
deck = genanki.Deck(deck_id, deck_name)
media_files_to_pack = []
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
with tempfile.TemporaryDirectory() as tmpdir:
for idx, record in enumerate(records):
en_text = record["en_text"]
es_text = record["es_text"]
gender_flag = record.get("gender", "Female")
# Map native neural voice files matching gender settings
spanish_voice = "es-ES-AlvaroNeural" if gender_flag == "Male" else "es-ES-ElviraNeural"
english_voice = "en-US-EmmaNeural"
raw_tags = record.get("tags") or ""
note_tags = [t.strip().replace(" ", "_") for t in raw_tags.split(",") if t.strip()]
# Dynamic HTML Notes Mapping Builder
notes_html_parts = []
if record.get('notes') and record['notes'].strip():
notes_html_parts.append(f"{record['notes'].strip()}
")
if record.get('source_context') and record['source_context'].strip():
notes_html_parts.append(f"Context: {record['source_context'].strip()}
")
notes_html = "".join(notes_html_parts)
# Safely isolate the raw text data string down to Anki
anki_notes_html = record['anki_notes'].strip() if record.get('anki_notes') else ""
# Standard Unique Media Filenames
en_audio_filename = f"edge_en_{idx}_{model_id}.mp3"
es_audio_filename = f"edge_es_{idx}_{model_id}.mp3"
en_audio_path = os.path.join(tmpdir, en_audio_filename)
es_audio_path = os.path.join(tmpdir, es_audio_filename)
# English Audio Synthesis
if loop.run_until_complete(generate_edge_audio(en_text, english_voice, en_audio_path, rate_string)):
media_files_to_pack.append(en_audio_path)
en_audio_field = f"[sound:{en_audio_filename}]"
else:
en_audio_field = ""
# Spanish Audio Synthesis
if loop.run_until_complete(generate_edge_audio(es_text, spanish_voice, es_audio_path, rate_string)):
media_files_to_pack.append(es_audio_path)
es_audio_field = f"[sound:{es_audio_filename}]"
else:
es_audio_field = ""
note = genanki.Note(
model=anki_model,
fields=[en_text, es_text, notes_html, anki_notes_html, en_audio_field, es_audio_field],
tags=note_tags
)
deck.add_note(note)
# Build Package while media assets are guaranteed contextually active inside tmpdir
package = genanki.Package(deck)
package.media_files = media_files_to_pack
package.write_to_file(output_path)