awacke1's picture
Update app.py
031a2ea verified
raw
history blame
14 kB
import streamlit as st
import random
import re
from gtts import gTTS
from PIL import Image, ImageDraw, ImageFont
import io
import base64
# Default word lists for storytelling classes
default_word_lists = {
"Location": ["quiet town", "small village", "city", "forest", "mountain"],
"Actions": ["walking", "pedaling", "running", "dancing", "exploring"],
"Thoughts": ["chasing shadows", "what if", "brilliance of years", "echoes", "secrets"],
"Emotions": ["joy", "pain", "trembling smile", "storm", "silent art"],
"Dialogue": ["\"Keep moving, dare to feel;\"", "\"Am I chasing shadows?\"", "\"The dawn awaits!\"", "\"I love you.\"", "\"Let’s go!\""]
}
# Suit properties for narrative flavor
suit_properties = {
"Hearts": "emotional or romantic",
"Diamonds": "wealthy or luxurious",
"Clubs": "conflict or struggle",
"Spades": "mysterious or dangerous"
}
# Sentence templates for story generation
sentence_templates = {
"Location": "The story unfolded in a {property} {word}.",
"Actions": "Suddenly, a {property} {word} changed everything.",
"Thoughts": "A {property} thought, '{word}', crossed their mind.",
"Emotions": "A {property} wave of {word} surged through them.",
"Dialogue": "Someone spoke with a {property} tone: {word}"
}
# Choice templates for branching narrative
choice_templates = {
"Location": ["Explore the {word} further.", "Leave the {word} behind."],
"Actions": ["Continue {word} despite the risk.", "Stop {word} and reconsider."],
"Thoughts": ["Pursue the '{word}' idea.", "Ignore the '{word}' thought."],
"Emotions": ["Embrace the {word}.", "Suppress the {word}."],
"Dialogue": ["Respond to {word}.", "Ignore {word} and move on."]
}
# Pure Python function to augment word lists from user input
def augment_word_lists(user_input):
augmented_lists = {key: list(set(val)) for key, val in default_word_lists.items()}
words = user_input.lower().split()
location_keywords = ["town", "village", "city", "forest", "mountain", "place", "land"]
action_keywords = ["walk", "run", "dance", "pedal", "explore", "move", "jump"]
emotion_keywords = ["joy", "pain", "smile", "storm", "fear", "love", "anger"]
dialogues = re.findall(r'"[^"]*"', user_input)
augmented_lists["Dialogue"].extend(dialogues)
for word in words:
if any(keyword in word for keyword in location_keywords):
augmented_lists["Location"].append(word)
elif any(keyword in word for keyword in action_keywords):
augmented_lists["Actions"].append(word)
elif any(keyword in word for keyword in emotion_keywords):
augmented_lists["Emotions"].append(word)
elif "?" in word or "what" in word or "why" in word:
augmented_lists["Thoughts"].append(word)
for key in augmented_lists:
augmented_lists[key] = list(set(augmented_lists[key]))
return augmented_lists
# Create a 52-card deck
def create_deck():
suits = ["Hearts", "Diamonds", "Clubs", "Spades"]
ranks = list(range(1, 14))
deck = [(suit, rank) for suit in suits for rank in ranks]
random.shuffle(deck)
return deck
# Assign cards to classes
def assign_card_to_class(card_index):
if 0 <= card_index < 10:
return "Location"
elif 10 <= card_index < 20:
return "Actions"
elif 20 <= card_index < 30:
return "Thoughts"
elif 30 <= card_index < 40:
return "Emotions"
else:
return "Dialogue"
# Generate card visualization with p5.js
def generate_card_visualization(suit, rank, story_class, word, property):
num_balls = rank * 5 # More balls for higher rank
jelly_size = {"Hearts": 40, "Diamonds": 50, "Clubs": 30, "Spades": 60}[suit] # Suit affects jellyfish size
rotation_speed = {"Location": 0.005, "Actions": 0.01, "Thoughts": 0.003, "Emotions": 0.007, "Dialogue": 0.009}[story_class]
hue_base = {"Hearts": 0, "Diamonds": 120, "Clubs": 240, "Spades": 300}[suit] # Suit affects color
html_code = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js"></script>
<style>
body {{ margin: 0; padding: 0; overflow: hidden; background: black; }}
#p5-container {{ display: flex; justify-content: center; align-items: center; }}
</style>
</head>
<body>
<div id="p5-container"></div>
<script>
let balls = [];
let jellyfish;
const sphereRadius = 150;
let sphereCenter;
let rotationAngle = 0;
const numBalls = {num_balls};
function setup() {{
let canvas = createCanvas(400, 400);
canvas.parent('p5-container');
sphereCenter = createVector(width/2, height/2);
colorMode(HSB, 360, 100, 100, 1);
for (let i = 0; i < numBalls; i++) {{
balls.push(new Ball());
}}
jellyfish = new Jellyfish();
}}
function draw() {{
background(0, 0, 0, 0.1);
rotationAngle += {rotation_speed};
push();
translate(sphereCenter.x, sphereCenter.y);
rotate(rotationAngle);
noFill();
stroke(255);
strokeWeight(2);
ellipse(0, 0, sphereRadius * 2, sphereRadius * 2);
for (let ball of balls) {{
ball.update();
ball.checkBoundaryCollision();
ball.display();
}}
jellyfish.update();
jellyfish.checkBoundaryCollision();
jellyfish.display();
pop();
// Card info overlay
fill(255, 255, 255, 0.8);
noStroke();
rect(0, 0, width, 60);
fill(0);
textSize(16);
textAlign(CENTER);
text("{rank} of {suit} - {story_class}: {word} ({property})", width/2, 30);
}}
class Ball {{
constructor() {{
this.r = 5;
let angle = random(TWO_PI);
let rad = random(sphereRadius - this.r);
this.pos = createVector(rad * cos(angle), rad * sin(angle));
let speed = random(1, 3);
let vAngle = random(TWO_PI);
this.vel = createVector(speed * cos(vAngle), speed * vAngle);
this.col = color({hue_base}, 100, 100);
}}
update() {{ this.pos.add(this.vel); }}
checkBoundaryCollision() {{
let d = this.pos.mag();
if (d + this.r > sphereRadius) {{
let normal = this.pos.copy().normalize();
let dot = this.vel.dot(normal);
this.vel.sub(p5.Vector.mult(normal, 2 * dot));
this.pos = normal.mult(sphereRadius - this.r);
}}
}}
display() {{
noStroke();
fill(this.col);
ellipse(this.pos.x, this.pos.y, this.r * 2, this.r * 2);
}}
}}
class Jellyfish {{
constructor() {{
this.size = {jelly_size};
this.pos = createVector(random(-sphereRadius + this.size, sphereRadius - this.size),
random(-sphereRadius + this.size, sphereRadius - this.size));
let speed = random(1, 2);
let angle = random(TWO_PI);
this.vel = createVector(speed * cos(angle), speed * sin(angle));
this.t = 0;
}}
update() {{ this.pos.add(this.vel); this.t += 0.05; }}
checkBoundaryCollision() {{
if (this.pos.mag() + this.size > sphereRadius) {{
let normal = this.pos.copy().normalize();
let dot = this.vel.dot(normal);
this.vel.sub(p5.Vector.mult(normal, 2 * dot));
this.pos = normal.mult(sphereRadius - this.size);
}}
}}
display() {{
push();
translate(this.pos.x, this.pos.y);
strokeWeight(1.5);
for (let y = 99; y < 300; y += 4) {{
for (let x = 99; x < 300; x += 2) {{
let res = jellyA(x, y, this.t);
let px = res[0] - 200;
let py = res[1] - 200;
stroke(getJellyColor(x, y, this.t));
point(px, py);
}}
}}
pop();
}}
}}
function jellyA(x, y, t) {{
let k = x / 8 - 25;
let e = y / 8 - 25;
let d = (k * k + e * e) / 99;
let q = x / 3 + k * 0.5 / cos(y * 5) * sin(d * d - t);
let c = d / 2 - t / 8;
let xPos = q * sin(c) + e * sin(d + k - t) + 200;
let yPos = (q + y / 8 + d * 9) * cos(c) + 200;
return [xPos, yPos];
}}
function getJellyColor(x, y, t) {{
let hue = (sin(t / 2) * 360 + x / 3 + y / 3) % 360;
let saturation = 70 + sin(t) * 30;
let brightness = 50 + cos(t / 2) * 20;
return color(hue, saturation, brightness, 0.5);
}}
</script>
</body>
</html>
"""
return html_code
# Generate story sentence
def generate_story_sentence(story_class, word, property):
return sentence_templates[story_class].format(word=word, property=property)
# Generate choice options
def generate_choices(story_class, word):
return [template.format(word=word) for template in choice_templates[story_class]]
# Generate song lyrics
def generate_song_lyrics(story_text):
words = story_text.split()
key_elements = [word for word in words if len(word) > 3][:12]
lyrics = "\n".join([f"{key_elements[i]} {key_elements[i+1]}" for i in range(0, len(key_elements)-1, 2)])
return lyrics
# Main app
def main():
st.set_page_config(page_title="StoryForge: The Animated Adventure", page_icon="🎴", layout="wide")
st.title("🎴 StoryForge: A Choose Your Own Adventure Game 🎴")
# User input
st.markdown("## πŸ“ Your Story Seed")
user_input = st.text_area("Paste your story inspiration here:", height=200)
# Session state initialization
if "augmented_lists" not in st.session_state:
st.session_state.augmented_lists = default_word_lists
if "deck" not in st.session_state:
st.session_state.deck = create_deck()
if "story" not in st.session_state:
st.session_state.story = []
if "drawn_cards" not in st.session_state:
st.session_state.drawn_cards = 0
if "history" not in st.session_state:
st.session_state.history = []
if "current_choices" not in st.session_state:
st.session_state.current_choices = []
if "last_card" not in st.session_state:
st.session_state.last_card = None
# Process input
if st.button("Start Game"):
if user_input:
st.session_state.augmented_lists = augment_word_lists(user_input)
st.session_state.deck = create_deck()
st.session_state.story = []
st.session_state.history = []
st.session_state.drawn_cards = 0
st.session_state.current_choices = []
st.session_state.last_card = None
st.success("Game started! Draw your first card.")
# Layout
col1, col2 = st.columns([2, 3])
with col1:
# Draw card
if st.button("Draw Card") and st.session_state.drawn_cards < 52:
card_index = st.session_state.drawn_cards
suit, rank = st.session_state.deck[card_index]
story_class = assign_card_to_class(card_index)
word = random.choice(st.session_state.augmented_lists[story_class])
property = suit_properties[suit]
# Generate and display animated visualization
viz_html = generate_card_visualization(suit, rank, story_class, word, property)
st.components.v1.html(viz_html, height=420, scrolling=False)
# Generate story sentence and choices
sentence = generate_story_sentence(story_class, word, property)
st.session_state.story.append(sentence)
st.session_state.current_choices = generate_choices(story_class, word)
st.session_state.last_card = (suit, rank, story_class, word, property)
st.session_state.drawn_cards += 1
# Display choices
if st.session_state.current_choices:
st.markdown("#### Make Your Choice:")
choice = st.radio("What happens next?", st.session_state.current_choices)
if st.button("Confirm Choice"):
st.session_state.history.append((st.session_state.story[-1], choice))
st.session_state.current_choices = []
st.success(f"Choice made: {choice}")
with col2:
st.markdown("### πŸ“œ Your Story Unfolds")
if st.session_state.story:
st.write("\n".join(st.session_state.story))
st.markdown("### πŸ•°οΈ Adventure History")
if st.session_state.history:
for i, (event, choice) in enumerate(st.session_state.history):
st.write(f"**Step {i+1}**: {event} β†’ *Choice: {choice}*")
# Song generation
if st.session_state.drawn_cards == 52:
full_story = "\n".join(st.session_state.story)
st.markdown("### 🎡 Story Song")
lyrics = generate_song_lyrics(full_story)
st.write(lyrics)
tts = gTTS(text=lyrics, lang="en")
audio_file = "story_song.mp3"
tts.save(audio_file)
st.audio(audio_file, format="audio/mp3")
if __name__ == "__main__":
main()