Spaces:
Sleeping
Sleeping
| 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() |