Spaces:
Sleeping
Sleeping
MilanCalegari
commited on
Commit
·
132cb66
1
Parent(s):
11f5776
feat: add base arcana functions
Browse files- app.py +103 -0
- config.yaml +3 -0
- modules/interfaces/llm_interface.py +14 -0
- modules/llm/__init__.py +0 -0
- modules/llm/card_interpreter.py +103 -0
- modules/tarot/__init__.py +0 -0
- modules/tarot/card.py +45 -0
- modules/utils/commom.py +50 -0
- scripts/get_cards.py +28 -0
- test/test_inference.py +12 -0
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)
|