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