2026-06-24 05:37:22 +00:00
|
|
|
# anki_exporter.py
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
import subprocess
|
|
|
|
|
import shutil
|
|
|
|
|
import genanki
|
|
|
|
|
|
|
|
|
|
def compile_anki_package(records, output_path, deck_name):
|
|
|
|
|
"""
|
|
|
|
|
Compiles database records into an .apkg package using native macOS TTS.
|
|
|
|
|
Uses 'Monica' for Spanish targets and the default premium system voice for English.
|
|
|
|
|
"""
|
|
|
|
|
# Create a unique random Model ID and Deck ID for genanki
|
|
|
|
|
model_id = 1684329011
|
|
|
|
|
deck_id = 1684329012
|
|
|
|
|
|
|
|
|
|
# Define the Anki Card Layout structure with audio fields
|
|
|
|
|
anki_model = genanki.Model(
|
|
|
|
|
model_id,
|
|
|
|
|
'Spanish Voice Trainer Model',
|
|
|
|
|
fields=[
|
|
|
|
|
{'name': 'EnglishText'},
|
|
|
|
|
{'name': 'SpanishText'},
|
|
|
|
|
{'name': 'Notes'},
|
|
|
|
|
{'name': 'EnglishAudio'},
|
|
|
|
|
{'name': 'SpanishAudio'}
|
|
|
|
|
],
|
|
|
|
|
templates=[
|
|
|
|
|
{
|
|
|
|
|
'name': 'Card 1',
|
|
|
|
|
'qfmt': '<div style="font-family: Arial; font-size: 20px; text-align: center; color: #34495E;">'
|
|
|
|
|
'Translate to Spanish:<br><br><b>{{EnglishText}}</b></div><div style="display:none;">{{EnglishAudio}}</div>',
|
|
|
|
|
'afmt': '{{FrontSide}}<hr id="answer">'
|
|
|
|
|
'<div style="font-family: Arial; font-size: 28px; text-align: center; color: #2980B9; font-weight: bold;">'
|
|
|
|
|
'{{SpanishText}}</div><br>'
|
|
|
|
|
'<div style="font-family: Arial; font-size: 14px; text-align: center; color: #7F8C8D; font-style: italic;">'
|
|
|
|
|
'{{Notes}}</div><br>'
|
|
|
|
|
'<div style="text-align: center;">{{SpanishAudio}}</div>',
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
deck = genanki.Deck(deck_id, deck_name)
|
|
|
|
|
media_files = []
|
|
|
|
|
|
|
|
|
|
# Process all records inside a secure temporary directory workspace
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
|
|
for idx, record in enumerate(records):
|
|
|
|
|
en_text = record["en_text"]
|
|
|
|
|
es_text = record["es_text"]
|
2026-06-26 06:20:45 +00:00
|
|
|
|
|
|
|
|
# Extract and parse tags for native Anki tag support (split comma-separated text)
|
|
|
|
|
raw_tags = record.get("tags") or ""
|
|
|
|
|
note_tags = [t.strip().replace(" ", "_") for t in raw_tags.split(",") if t.strip()]
|
|
|
|
|
|
|
|
|
|
# Build text for visual notes area, appending tags if they exist
|
|
|
|
|
notes_parts = []
|
|
|
|
|
if record.get('source_context'):
|
|
|
|
|
notes_parts.append(f"Context: {record['source_context']}")
|
|
|
|
|
if record.get('notes'):
|
|
|
|
|
notes_parts.append(f"Notes: {record['notes']}")
|
|
|
|
|
if raw_tags:
|
|
|
|
|
notes_parts.append(f"Tags: {raw_tags}")
|
|
|
|
|
|
|
|
|
|
notes_display_text = " | ".join(notes_parts)
|
2026-06-24 05:37:22 +00:00
|
|
|
|
|
|
|
|
# Generate unique filenames for the media assets
|
|
|
|
|
en_audio_filename = f"en_audio_{idx}.mp3"
|
|
|
|
|
es_audio_filename = f"es_audio_{idx}.mp3"
|
|
|
|
|
|
|
|
|
|
en_audio_path = os.path.join(tmpdir, en_audio_filename)
|
|
|
|
|
es_audio_path = os.path.join(tmpdir, es_audio_filename)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# 1. Render English Audio using native macOS text-to-speech engine
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["say", "-o", en_audio_path, "--data-format=Iface", en_text],
|
|
|
|
|
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
|
|
|
)
|
|
|
|
|
media_files.append(en_audio_path)
|
|
|
|
|
en_audio_field = f"[sound:{en_audio_filename}]"
|
|
|
|
|
except Exception:
|
|
|
|
|
en_audio_field = ""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# 2. Render Spanish Audio explicitly targeting the Monica voice profile
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["say", "-v", "Monica", "-o", es_audio_path, "--data-format=Iface", es_text],
|
|
|
|
|
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
|
|
|
)
|
|
|
|
|
media_files.append(es_audio_path)
|
|
|
|
|
es_audio_field = f"[sound:{es_audio_filename}]"
|
|
|
|
|
except Exception:
|
|
|
|
|
es_audio_field = ""
|
|
|
|
|
|
2026-06-26 06:20:45 +00:00
|
|
|
# Build the card note stack, natively injecting the parsed Anki tags list
|
2026-06-24 05:37:22 +00:00
|
|
|
note = genanki.Note(
|
|
|
|
|
model=anki_model,
|
2026-06-26 06:20:45 +00:00
|
|
|
fields=[en_text, es_text, notes_display_text, en_audio_field, es_audio_field],
|
|
|
|
|
tags=note_tags
|
2026-06-24 05:37:22 +00:00
|
|
|
)
|
|
|
|
|
deck.add_note(note)
|
|
|
|
|
|
|
|
|
|
# Build package collection mapping archive pipelines
|
|
|
|
|
package = genanki.Package(deck)
|
|
|
|
|
package.media_files = media_files
|
2026-06-26 06:20:45 +00:00
|
|
|
package.write_to_file(output_path)
|