import os
import random
import logging
import genanki
from datetime import datetime
from typing import List, Tuple, Optional
from .models import GermanWord, UnsplashImage
from .clients.llm import AnthropicClient
from .clients.unsplash import UnsplashClient
logger = logging.getLogger(__name__)
class GermanDeckPackage:
"""Package class to create Anki deck with media files"""
def __init__(self, deck, media_files):
self.deck = deck
self.media_files = media_files
def write_to_file(self, file):
"""Write deck to a file"""
package = genanki.Package([self.deck])
package.media_files = self.media_files
logger.info("Writing deck with media files: %s", self.media_files)
package.write_to_file(file)
class CardGenerator:
def __init__(self, llm_client: AnthropicClient, unsplash_client: UnsplashClient):
self.llm_client = llm_client
self.unsplash_client = unsplash_client
self.model = self._create_model()
def _create_model(self):
"""Create the Anki note model"""
return genanki.Model(
random.randrange(1 << 30, 1 << 31),
'German Vocabulary Model',
fields=[
{'name': 'German_Word'},
{'name': 'Part_of_Speech'},
{'name': 'Source'},
{'name': 'English_Meaning'},
{'name': 'Article'},
{'name': 'Plural_Form'},
{'name': 'Example_Sentence'},
{'name': 'Sentence_Translation'},
{'name': 'Usage_Notes'},
{'name': 'Related_Words'},
{'name': 'Image'},
{'name': 'Image_Credit'},
{'name': 'Tags'}
],
templates=[
{
'name': 'German Vocabulary Card',
'qfmt': '''
{{German_Word}} {{tts de_DE:German_Word}}
Part of speech: {{Part_of_Speech}}
Source: {{Source}}
{{Image}}
''',
'afmt': '''
{{FrontSide}}
English meaning: {{English_Meaning}}
Article: {{Article}}
Plural Form: {{Plural_Form}}
Example: {{Example_Sentence}} {{tts de_DE:Example_Sentence}}
Translation: {{Sentence_Translation}}
Usage notes: {{Usage_Notes}}
Related words: {{Related_Words}}
{{#Image_Credit}}Photo: {{Image_Credit}}
{{/Image_Credit}}
Tags: {{Tags}}
'''
}
],
css='''
.card {
font-family: arial;
font-size: 16px;
text-align: center;
color: black;
background-color: white;
}
img {
margin: 20px auto;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
'''
)
def _load_processed_words(self) -> set:
"""Load previously processed words from tracking file"""
processed_words = set()
tracking_file = os.path.join("output", "processed_words.txt")
if os.path.exists(tracking_file):
with open(tracking_file, 'r', encoding='utf-8') as f:
processed_words = set(line.strip() for line in f if line.strip())
logger.info("Found %d previously processed words", len(processed_words))
return processed_words
def _add_to_processed_words(self, word: str):
"""Add a word to the tracking file"""
tracking_file = os.path.join("output", "processed_words.txt")
os.makedirs("output", exist_ok=True)
with open(tracking_file, 'a', encoding='utf-8') as f:
f.write(f"{word}\n")
logger.debug("Added %s to processed words", word)
def create_deck(self, word_list: List[Tuple[str, str]], deck_name: str = "German Vocabulary") -> Tuple[genanki.Deck, List[str]]:
"""Create an Anki deck from a list of words"""
deck_id = random.randrange(1 << 30, 1 << 31)
deck = genanki.Deck(deck_id, deck_name)
media_files = []
# Load previously processed words
processed_words = self._load_processed_words()
for word, source in word_list:
# Skip if word has been processed before
if word in processed_words:
logger.info("Skipping %s - already exists in previous deck", word)
continue
try:
card_info_dict = self.llm_client.get_card_info(word, source)
card_info = GermanWord(**card_info_dict)
image_filename = f"{word.lower().replace(' ', '_')}.jpg"
# Create output directory if it doesn't exist
output_dir = "output"
os.makedirs(output_dir, exist_ok=True)
media_dir = "media"
os.makedirs(media_dir, exist_ok=True)
# Save image directly to media directory
image_path = os.path.join(media_dir, image_filename)
image = self.unsplash_client.get_image(
card_info.image_search_term,
image_path
)
if image and image.local_path and os.path.exists(image.local_path):
# Use the full path for the media file
media_files.append(image.local_path)
logger.info("Added image: %s", image.local_path)
else:
image_path = ""
logger.warning("No image for: %s", word)
# Create note
note = genanki.Note(
model=self.model,
fields=[
card_info.german_word,
card_info.part_of_speech,
source,
card_info.english_meaning,
card_info.article or "",
card_info.plural_form,
card_info.example_sentence,
card_info.sentence_translation,
card_info.usage_notes,
card_info.related_words,
f'
',
image.photographer if image else "",
' '.join(card_info.tags)
]
)
deck.add_note(note)
# Track the word after successfully creating the note
self._add_to_processed_words(word)
logger.info("Added card for: %s", word)
except Exception as e:
logger.error("Error creating card for %s: %s", word, str(e))
return deck, media_files