184 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			184 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Python
		
	
	
	
| 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': '''
 | |
|                         <div style="font-size: 24px;">
 | |
|                             {{German_Word}} {{tts de_DE:German_Word}}
 | |
|                         </div>
 | |
|                         <div style="font-size: 18px;">Part of speech: {{Part_of_Speech}}</div>
 | |
|                         <div style="font-size: 14px;">Source: {{Source}}</div>
 | |
|                         {{Image}}
 | |
|                     ''',
 | |
|                     'afmt': '''
 | |
|                         {{FrontSide}}
 | |
|                         <hr id="answer">
 | |
|                         <div><b>English meaning:</b> {{English_Meaning}}</div>
 | |
|                         <div><b>Article:</b> {{Article}}</div>
 | |
|                         <div><b>Plural Form:</b> {{Plural_Form}}</div>
 | |
|                         <div>
 | |
|                             <b>Example:</b> {{Example_Sentence}} {{tts de_DE:Example_Sentence}}
 | |
|                         </div>
 | |
|                         <div><b>Translation:</b> {{Sentence_Translation}}</div>
 | |
|                         <div><b>Usage notes:</b> {{Usage_Notes}}</div>
 | |
|                         <div><b>Related words:</b> {{Related_Words}}</div>
 | |
|                         {{#Image_Credit}}<div><small>Photo: {{Image_Credit}}</small></div>{{/Image_Credit}}
 | |
|                         <div><small>Tags: {{Tags}}</small></div>
 | |
|                     '''
 | |
|                 }
 | |
|             ],
 | |
|             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'<img src="{os.path.basename(image.local_path) if image and image.local_path else ""}" />',
 | |
|                         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
 |