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
|