MilanCalegari commited on
Commit
132cb66
·
1 Parent(s): 11f5776

feat: add base arcana functions

Browse files
app.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import yaml
3
+
4
+ from modules.llm.card_interpreter import CardInterpreter
5
+ from modules.tarot.card import TarotDeck
6
+ from modules.utils.commom import CardReadingMethod, label4method
7
+
8
+ # Initialize deck and interpreter
9
+ deck = TarotDeck()
10
+ deck.get_cards()
11
+ interpreter = CardInterpreter()
12
+
13
+ st.set_page_config(
14
+ page_title="Tarot Reading",
15
+ page_icon="🔮",
16
+ layout="centered"
17
+ )
18
+
19
+ st.title("🔮 Tarot Reading")
20
+
21
+ # Secret configurations
22
+ with st.sidebar:
23
+ with st.expander("⚙️ Settings", expanded=False):
24
+ reversed_prob = st.slider(
25
+ "Probability of reversed cards",
26
+ min_value=0.0,
27
+ max_value=1.0,
28
+ value=yaml.safe_load(open("config.yaml"))["reverse_probability"],
29
+ step=0.1,
30
+ help="Probability of a card appearing reversed (0.0 to 1.0)"
31
+ )
32
+ # TODO: Add Portuguese language support and translation
33
+ # language = st.selectbox(
34
+ # "Language",
35
+ # ["English", "Portuguese"],
36
+ # index=0
37
+ # )
38
+
39
+ reversed_prob -= 1
40
+
41
+ # User interface texts
42
+ welcome_text = "### Welcome to your Tarot Reading"
43
+ instructions_text = "Please select a reading method and provide a context for your consultation."
44
+ method_text = "Choose your reading method:"
45
+ context_text = "What would you like to know about? (Optional)"
46
+ context_placeholder = "Ex: I need guidance about finding my life purpose..."
47
+ draw_button = "Draw Cards"
48
+ spinner_texts = {
49
+ "shuffle": "🔮 Shuffling the cards with mystical energy...",
50
+ "channel": "✨ Channeling the energies of the universe...",
51
+ "reveal": "🌟 Revealing the secrets of destiny...",
52
+ "consult": "🧙‍♂️ Consulting ancient wisdom...",
53
+ "cards": "### Your Cards:",
54
+ "reading": "### Your Reading:",
55
+ "default_context": "General daily reading"
56
+ }
57
+
58
+ # Display welcome message and instructions
59
+ st.markdown(welcome_text)
60
+ st.markdown(instructions_text)
61
+
62
+ # Reading method selection
63
+ method = st.selectbox(
64
+ method_text,
65
+ [
66
+ CardReadingMethod.PAST_PRESENT_FUTURE.value,
67
+ CardReadingMethod.CELTIC_CROSS.value,
68
+ CardReadingMethod.HAND_OF_ERIS.value
69
+ ]
70
+ )
71
+
72
+ # Reading context input
73
+ context = st.text_area(
74
+ context_text,
75
+ placeholder=context_placeholder
76
+ )
77
+
78
+ if st.button(draw_button):
79
+ # Shuffle and draw cards
80
+ with st.spinner(spinner_texts["shuffle"]):
81
+ deck.shuffle(reversed_prob)
82
+
83
+ with st.spinner(spinner_texts["channel"]):
84
+ cards = deck.draw(CardReadingMethod(method))
85
+
86
+ # Display cards
87
+ st.markdown(spinner_texts["cards"])
88
+
89
+ cols = st.columns(len(cards))
90
+ for idx, (card, col) in enumerate(zip(cards, cols)):
91
+ with col:
92
+ with st.spinner(spinner_texts["reveal"]):
93
+ st.image(card.image_pth, caption=f"{label4method[CardReadingMethod(method)][idx]}: {card.name}")
94
+
95
+ # Generate and display interpretation
96
+ with st.spinner(spinner_texts["consult"]):
97
+ if context:
98
+ interpretation = interpreter.generate_interpretation(cards, context, CardReadingMethod(method))
99
+ else:
100
+ interpretation = interpreter.generate_interpretation(cards, None, CardReadingMethod(method))
101
+
102
+ st.markdown(spinner_texts["reading"])
103
+ st.write(interpretation)
config.yaml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Probability of reversed cards (0.0 to 1.0)
2
+ reverse_probability: 0.3
3
+
modules/interfaces/llm_interface.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, List, Optional
3
+
4
+ from modules.utils.commom import Card, CardReadingMethod
5
+
6
+
7
+ class CardInterpreterInterface(ABC):
8
+ @abstractmethod
9
+ def generate_interpretation(self, cards: List[Card], context: Optional[str], method: CardReadingMethod) -> str:
10
+ ...
11
+
12
+ @abstractmethod
13
+ def generate_prompt(self, cards: List[Card], context: str, method: CardReadingMethod) -> List[Dict[str, str]]:
14
+ ...
modules/llm/__init__.py ADDED
File without changes
modules/llm/card_interpreter.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Optional
2
+
3
+ from transformers import pipeline
4
+
5
+ from modules.utils.commom import Card, CardReadingMethod
6
+
7
+ from ..interfaces.llm_interface import CardInterpreterInterface
8
+
9
+
10
+ class CardInterpreter(CardInterpreterInterface):
11
+ def __init__(self) -> None:
12
+ # Initialize pipeline once and cache it
13
+ self.pipeline = pipeline(
14
+ "text-generation",
15
+ model="meta-llama/Llama-3.2-1B-Instruct",
16
+ device_map="auto",
17
+ pad_token_id=128001,
18
+ )
19
+
20
+ # Cache base prompt content that doesn't change
21
+ self._base_content = """
22
+ You are a powerful occultist and exceptional tarot reader. Provide a concise reading based on the given cards.
23
+
24
+ Focus on Rider Waite Tarot symbolism and card imagery.
25
+
26
+ The possible methods are:
27
+ - PAST_PRESENT_FUTURE: Three cards, past, present and future;
28
+ - CELTIC_CROSS: Ten cards, the situation, challenges, what to focus on, your past, your strengths, near future, suggested approach, what you need to know, your hopes and fears, and outcomes;
29
+ - HAND_OF_ERIS: Five cards, about your question, what may help you, what may hinder you, possible outcome number 1, and possible outcome number 2;
30
+
31
+ Reading Guidelines:
32
+ - Keep answers brief and focused;
33
+ - Provide a summary overview;
34
+ - Only interpret reversed cards when specified;
35
+ - With context: Focus on context-specific interpretation;
36
+ - Without context: Give practical daily guidance;
37
+
38
+ If the context is General reading:
39
+ - Provide a general daily life reading;
40
+ - Focus on practical matters;
41
+
42
+ If other context is provided:
43
+ - Focus on the context provided;
44
+ - Provide a reading related to the context;
45
+ - Focus primarily on interpreting within the given context
46
+ - Keep the symbolism of the cards first, but make sure to use the context to interpret the cards.
47
+ """
48
+
49
+ def _format_card(self, card: Card) -> str:
50
+ # Helper to format card name
51
+ return f"{card.name} (Reversed)" if card.reversed else card.name
52
+
53
+ def generate_prompt(
54
+ self, cards: List[Card], context: str, method: CardReadingMethod
55
+ ) -> List[Dict[str, str]]:
56
+ method_templates = {
57
+ CardReadingMethod.PAST_PRESENT_FUTURE: lambda: f"""The provided cards are:
58
+ Past: {self._format_card(cards[0])}
59
+ Present: {self._format_card(cards[1])}
60
+ Future: {self._format_card(cards[2])}
61
+ """,
62
+
63
+ CardReadingMethod.CELTIC_CROSS: lambda: f"""The provided cards are:
64
+ The situation: {self._format_card(cards[0])}
65
+ Challenges: {self._format_card(cards[1])}
66
+ What to focus on: {self._format_card(cards[2])}
67
+ Your past: {self._format_card(cards[3])}
68
+ Your strengths: {self._format_card(cards[4])}
69
+ Near future: {self._format_card(cards[5])}
70
+ Suggested approach: {self._format_card(cards[6])}
71
+ What you need to know: {self._format_card(cards[7])}
72
+ Your hopes and fears: {self._format_card(cards[8])}
73
+ Outcomes: {self._format_card(cards[9])}
74
+ """,
75
+
76
+ CardReadingMethod.HAND_OF_ERIS: lambda: f"""The provided cards are:
77
+ About your question: {self._format_card(cards[0])}
78
+ What may help you: {self._format_card(cards[1])}
79
+ What may hinder you: {self._format_card(cards[2])}
80
+ Possible outcome number 1: {self._format_card(cards[3])}
81
+ Possible outcome number 2: {self._format_card(cards[4])}
82
+ """
83
+ }
84
+
85
+ question = method_templates[method]()
86
+ question += f"\nIn the context of: {context}\nDrawn with the method: {method.value}"
87
+
88
+ return [
89
+ {"role": "system", "content": self._base_content},
90
+ {"role": "user", "content": question}
91
+ ]
92
+
93
+ def generate_interpretation(
94
+ self, cards: List[Card], context: Optional[str], method: CardReadingMethod
95
+ ) -> str:
96
+ prompt = self.generate_prompt(cards, context or "General reading", method)
97
+ result = self.pipeline(
98
+ prompt,
99
+ max_new_tokens=512, # Limit token generation
100
+ num_return_sequences=1, # Only generate one response
101
+ do_sample=False # Deterministic output
102
+ )
103
+ return result[0]["generated_text"][-1]["content"]
modules/tarot/__init__.py ADDED
File without changes
modules/tarot/card.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import random
3
+
4
+ from ..utils.commom import Card, CardReadingMethod
5
+
6
+
7
+ class TarotDeck:
8
+ def __init__(self):
9
+ with open("./data/tarot-images.json") as f:
10
+ self.cards_json = json.load(f)
11
+
12
+ def get_cards(self):
13
+ cards = []
14
+ for card_data in self.cards_json["cards"]:
15
+ name = card_data["name"]
16
+ number = int(card_data["number"])
17
+ is_major_arcana = card_data["arcana"]
18
+ image_pth = f"./data/cards/{card_data['img']}"
19
+
20
+ card = Card(
21
+ name=name,
22
+ number=number,
23
+ is_major_arcana=is_major_arcana,
24
+ image_pth=image_pth,
25
+ )
26
+ cards.append(card)
27
+
28
+ self.cards = cards
29
+
30
+ def shuffle(self, reversal_prob: float):
31
+ random.shuffle(self.cards)
32
+ for card in self.cards:
33
+ card.reversed = random.random() < reversal_prob
34
+
35
+ return self.cards
36
+
37
+ def draw(self, method: CardReadingMethod = CardReadingMethod.PAST_PRESENT_FUTURE):
38
+ if method == CardReadingMethod.PAST_PRESENT_FUTURE:
39
+ return self.cards[0:3]
40
+ elif method == CardReadingMethod.CELTIC_CROSS:
41
+ return self.cards[0:10]
42
+ elif method == CardReadingMethod.HAND_OF_ERIS:
43
+ return self.cards[0:5]
44
+ else:
45
+ raise ValueError(f"Invalid method: {method}")
modules/utils/commom.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ArcanaType(str, Enum):
7
+ MAJOR = "Major Arcana"
8
+ MINOR = "Minor Arcana"
9
+
10
+
11
+ class CardReadingMethod(str, Enum):
12
+ PAST_PRESENT_FUTURE = "past_present_future"
13
+ CELTIC_CROSS = "celtic_cross"
14
+ HAND_OF_ERIS = "hand_of_eris"
15
+
16
+
17
+ label4method = {
18
+ CardReadingMethod.PAST_PRESENT_FUTURE: ["Past", "Present", "Future"],
19
+ CardReadingMethod.CELTIC_CROSS: [
20
+ "Situation",
21
+ "Potential/Challenges",
22
+ "Focus",
23
+ "Past",
24
+ "Possibilities",
25
+ "Near Future",
26
+ "Power",
27
+ "Environment",
28
+ "Hopes/Fears",
29
+ "Outcomes",
30
+ ],
31
+ CardReadingMethod.HAND_OF_ERIS: [
32
+ "About your question",
33
+ "What may help you",
34
+ "What may hinder you",
35
+ "Possible outcome number 1",
36
+ "Possible outcome number 2",
37
+ ],
38
+ }
39
+
40
+
41
+ class Card(BaseModel):
42
+ name: str
43
+ number: int
44
+ is_major_arcana: ArcanaType
45
+ reversed: bool = False
46
+ image_pth: str
47
+
48
+ @property
49
+ def is_major(self) -> bool:
50
+ return self.is_major_arcana == ArcanaType.MAJOR
scripts/get_cards.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import zipfile
4
+
5
+ import requests
6
+
7
+
8
+ def get_cards():
9
+ # Create data directory
10
+ os.makedirs('./data', exist_ok=True)
11
+
12
+ # Download zip file
13
+ url = 'https://www.kaggle.com/api/v1/datasets/download/lsind18/tarot-json'
14
+ zip_file = './tarot-json.zip'
15
+
16
+ response = requests.get(url)
17
+ with open(zip_file, 'wb') as f:
18
+ f.write(response.content)
19
+
20
+ # Extract contents
21
+ with zipfile.ZipFile(zip_file, 'r') as zip_ref:
22
+ zip_ref.extractall('./data')
23
+
24
+ # Remove zip file
25
+ os.remove(zip_file)
26
+
27
+ if __name__ == '__main__':
28
+ get_cards()
test/test_inference.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from modules.llm.card_interpreter import CardInterpreter
2
+ from utils.commom import Card
3
+
4
+ cards = [
5
+ Card(name="The Fool", number=0, is_major_arcana=True, image_pth="", reversed=True),
6
+ Card(name="Five of Cups", number=5, is_major_arcana=False, image_pth="", reversed=False),
7
+ Card(name="The Lovers", number=6, is_major_arcana=False, image_pth="", reversed=False),
8
+ ]
9
+
10
+ interpreter = CardInterpreter()
11
+ response = interpreter.generate_interpretation(cards, "I'm feeling sad and lost")
12
+ print(response)