awacke1's picture
Update app.py
031a2ea verified
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()