Spaces:
Running
Running
# app.py - Final Deployment-Ready Version | |
import streamlit as st | |
import os | |
import time | |
import base64 | |
import random | |
import tempfile | |
import subprocess | |
import numpy as np | |
import pygame | |
import sys | |
import imageio | |
from PIL import Image | |
import io | |
import matplotlib.pyplot as plt | |
# Configure Streamlit page | |
st.set_page_config( | |
page_title="StoryCoder - Learn Python Through Stories", | |
page_icon="π§ββοΈ", | |
layout="wide", | |
initial_sidebar_state="expanded" | |
) | |
# Custom CSS for colorful UI | |
st.markdown(""" | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Comic+Neue:wght@700&display=swap'); | |
:root { | |
--primary: #FF6B6B; | |
--secondary: #4ECDC4; | |
--accent: #FFD166; | |
--dark: #1A535C; | |
--light: #F7FFF7; | |
} | |
body { | |
background: linear-gradient(135deg, var(--light) 0%, #E8F4F8 100%); | |
font-family: 'Comic Neue', cursive; | |
} | |
.stApp { | |
background: url('https://www.transparenttextures.com/patterns/cartographer.png'); | |
} | |
.story-box { | |
background-color: white; | |
border-radius: 20px; | |
padding: 25px; | |
box-shadow: 0 8px 16px rgba(26, 83, 92, 0.15); | |
border: 3px solid var(--accent); | |
margin-bottom: 25px; | |
} | |
.header { | |
color: var(--dark); | |
text-shadow: 2px 2px 4px rgba(0,0,0,0.1); | |
} | |
.concept-card { | |
background: linear-gradient(145deg, #ffffff, #f0f0f0); | |
border-radius: 15px; | |
padding: 15px; | |
margin: 10px 0; | |
border-left: 5px solid var(--secondary); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.05); | |
} | |
.stButton>button { | |
background: linear-gradient(45deg, var(--primary), var(--secondary)); | |
color: white; | |
border-radius: 12px; | |
padding: 10px 24px; | |
font-weight: bold; | |
font-size: 18px; | |
border: none; | |
transition: all 0.3s; | |
} | |
.stButton>button:hover { | |
transform: scale(1.05); | |
box-shadow: 0 6px 12px rgba(0,0,0,0.15); | |
} | |
.stTextInput>div>div>input { | |
border-radius: 12px; | |
padding: 12px; | |
border: 2px solid var(--accent); | |
} | |
.tabs { | |
display: flex; | |
gap: 10px; | |
margin-bottom: 20px; | |
overflow-x: auto; | |
} | |
.tab { | |
padding: 10px 20px; | |
background-color: var(--accent); | |
border-radius: 10px; | |
cursor: pointer; | |
font-weight: bold; | |
white-space: nowrap; | |
} | |
.tab.active { | |
background-color: var(--secondary); | |
color: white; | |
} | |
@media (max-width: 768px) { | |
.tabs { | |
flex-wrap: wrap; | |
} | |
} | |
.animation-container { | |
background-color: #1a1a2e; | |
border-radius: 15px; | |
padding: 20px; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
margin-bottom: 25px; | |
position: relative; | |
overflow: hidden; | |
} | |
.animation-canvas { | |
border-radius: 10px; | |
overflow: hidden; | |
margin: 0 auto; | |
display: block; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# Concept database | |
CONCEPTS = { | |
"loop": { | |
"name": "Loop", | |
"emoji": "π", | |
"description": "Loops repeat actions multiple times", | |
"example": "for i in range(5):\n print('Hello!')", | |
"color": "#FF9E6D" | |
}, | |
"conditional": { | |
"name": "Conditional", | |
"emoji": "β", | |
"description": "Conditionals make decisions in code", | |
"example": "if sunny:\n go_outside()\nelse:\n stay_inside()", | |
"color": "#4ECDC4" | |
}, | |
"function": { | |
"name": "Function", | |
"emoji": "β¨", | |
"description": "Functions are reusable blocks of code", | |
"example": "def greet(name):\n print(f'Hello {name}!')", | |
"color": "#FFD166" | |
}, | |
"variable": { | |
"name": "Variable", | |
"emoji": "π¦", | |
"description": "Variables store information", | |
"example": "score = 10\nplayer = 'Alex'", | |
"color": "#FF6B6B" | |
}, | |
"list": { | |
"name": "List", | |
"emoji": "π", | |
"description": "Lists store collections of items", | |
"example": "fruits = ['apple', 'banana', 'orange']", | |
"color": "#1A535C" | |
} | |
} | |
# Character database | |
CHARACTERS = { | |
"rabbit": {"color": (255, 150, 150), "speed": 5, "size": 30}, | |
"dragon": {"color": (255, 100, 100), "speed": 3, "size": 60}, | |
"cat": {"color": (200, 150, 255), "speed": 6, "size": 25}, | |
"dog": {"color": (255, 200, 150), "speed": 7, "size": 35}, | |
"knight": {"color": (150, 200, 255), "speed": 4, "size": 40}, | |
"wizard": {"color": (200, 255, 150), "speed": 4, "size": 35}, | |
"scientist": {"color": (150, 255, 200), "speed": 5, "size": 30}, | |
"pirate": {"color": (255, 255, 150), "speed": 5, "size": 40} | |
} | |
def analyze_story(story): | |
"""Analyze story and identify programming concepts""" | |
story_lower = story.lower() | |
detected_concepts = [] | |
# Check for loops | |
if any(word in story_lower for word in ["times", "repeat", "again", "multiple"]): | |
detected_concepts.append("loop") | |
# Check for conditionals | |
if any(word in story_lower for word in ["if", "when", "unless", "whether"]): | |
detected_concepts.append("conditional") | |
# Check for functions | |
if any(word in story_lower for word in ["make", "create", "do", "perform", "cast"]): | |
detected_concepts.append("function") | |
# Check for variables | |
if any(word in story_lower for word in ["is", "has", "set to", "value"]): | |
detected_concepts.append("variable") | |
# Check for lists | |
if any(word in story_lower for word in ["and", "many", "several", "collection", "items"]): | |
detected_concepts.append("list") | |
return list(set(detected_concepts)) | |
def generate_pygame_animation(story, concepts): | |
"""Generate PyGame animation based on story and concepts""" | |
try: | |
# Choose a random character | |
character = random.choice(list(CHARACTERS.keys())) | |
char_details = CHARACTERS[character] | |
# Extract count from story | |
count = 3 | |
for word in story.split(): | |
if word.isdigit(): | |
count = min(int(word), 10) | |
break | |
# Create a temporary file for the animation | |
with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as tmpfile: | |
gif_path = tmpfile.name | |
# Set up PyGame in headless mode | |
os.environ['SDL_VIDEODRIVER'] = 'dummy' | |
pygame.init() | |
pygame.display.set_mode((1, 1)) # Tiny invisible window | |
# Animation settings | |
WIDTH, HEIGHT = 800, 600 | |
screen = pygame.Surface((WIDTH, HEIGHT)) | |
clock = pygame.time.Clock() | |
frames = [] | |
# Colors | |
BACKGROUND = (25, 25, 50) | |
GROUND = (40, 100, 40) | |
SUN = (255, 255, 200) | |
# Character setup | |
char_x = 100 | |
char_y = HEIGHT - 100 | |
target_x = 700 | |
target_y = HEIGHT - 100 | |
hops = 0 | |
hop_height = 0 | |
# Animation loop | |
for frame in range(120): | |
# Clear screen | |
screen.fill(BACKGROUND) | |
# Draw ground | |
pygame.draw.rect(screen, GROUND, (0, HEIGHT - 50, WIDTH, 50)) | |
# Draw sun | |
pygame.draw.circle(screen, SUN, (700, 100), 50) | |
# Draw target | |
pygame.draw.circle(screen, (255, 200, 100), (target_x, target_y), 30) | |
pygame.draw.circle(screen, (255, 150, 50), (target_x, target_y), 20) | |
pygame.draw.circle(screen, (255, 100, 0), (target_x, target_y), 10) | |
# Move character | |
if hops < count: | |
char_x += char_details["speed"] | |
# Hop animation | |
hop_height = 50 * np.sin(frame * 0.2) | |
char_y = HEIGHT - 100 - abs(hop_height) | |
# Check if target reached | |
if char_x >= target_x - 40: | |
hops += 1 | |
char_x = 100 | |
# Draw character | |
pygame.draw.circle(screen, char_details["color"], (int(char_x), int(char_y)), char_details["size"]) | |
# Draw eyes | |
pygame.draw.circle(screen, (255, 255, 255), (int(char_x + 10), int(char_y - 5)), 8) | |
pygame.draw.circle(screen, (0, 0, 0), (int(char_x + 10), int(char_y - 5)), 4) | |
# Draw hop counter | |
font = pygame.font.SysFont(None, 36) | |
text = font.render(f"Hops: {hops}/{count}", True, (255, 255, 255)) | |
screen.blit(text, (20, 20)) | |
# Draw story text | |
story_text = font.render(story[:40] + ("..." if len(story) > 40 else ""), True, (200, 200, 255)) | |
screen.blit(story_text, (WIDTH//2 - story_text.get_width()//2, 50)) | |
# Capture frame | |
frame_data = pygame.surfarray.array3d(screen).swapaxes(0, 1) | |
frames.append(frame_data) | |
clock.tick(30) | |
# Save as GIF | |
imageio.mimsave(gif_path, frames, fps=30) | |
return gif_path | |
except Exception as e: | |
st.error(f"Animation error: {str(e)}") | |
return None | |
def create_story_image(story): | |
"""Create a story image with Matplotlib""" | |
try: | |
# Create figure | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
ax.set_facecolor('#f0f8ff') | |
ax.set_xlim(0, 10) | |
ax.set_ylim(0, 10) | |
ax.axis('off') | |
# Add title | |
ax.text(5, 8, 'β¨ Your Story β¨', fontsize=24, | |
ha='center', color='purple', fontweight='bold') | |
# Add story text (wrapped) | |
words = story.split() | |
lines = [] | |
current_line = [] | |
for word in words: | |
if len(' '.join(current_line + [word])) < 60: | |
current_line.append(word) | |
else: | |
lines.append(' '.join(current_line)) | |
current_line = [word] | |
if current_line: | |
lines.append(' '.join(current_line)) | |
for i, line in enumerate(lines): | |
ax.text(5, 6.5 - i*0.7, line, fontsize=14, ha='center') | |
# Add decoration | |
ax.text(2, 2, 'π°', fontsize=40, ha='center') | |
ax.text(8, 2, 'π', fontsize=40, ha='center') | |
ax.text(5, 1, 'Created with StoryCoder', fontsize=12, | |
ha='center', style='italic', color='gray') | |
# Save to buffer | |
buf = io.BytesIO() | |
plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0.5) | |
buf.seek(0) | |
image = Image.open(buf) | |
plt.close() | |
return image | |
except Exception as e: | |
st.error(f"Image generation error: {str(e)}") | |
return None | |
def main(): | |
"""Main application function""" | |
st.title("π§ββοΈ StoryCoder - Learn Python Through Stories!") | |
st.subheader("Turn your story into an animation and discover coding secrets!") | |
# Initialize session state | |
if 'story' not in st.session_state: | |
st.session_state.story = "" | |
if 'concepts' not in st.session_state: | |
st.session_state.concepts = [] | |
if 'animation_path' not in st.session_state: | |
st.session_state.animation_path = None | |
if 'active_tab' not in st.session_state: | |
st.session_state.active_tab = "story" | |
# Create tabs | |
tabs = st.empty() | |
tab_cols = st.columns(5) | |
with tab_cols[0]: | |
if st.button("π Create Story"): | |
st.session_state.active_tab = "story" | |
with tab_cols[1]: | |
if st.button("π¬ Animation"): | |
st.session_state.active_tab = "animation" | |
with tab_cols[2]: | |
if st.button("π Concepts"): | |
st.session_state.active_tab = "concepts" | |
with tab_cols[3]: | |
if st.button("π» Code"): | |
st.session_state.active_tab = "code" | |
with tab_cols[4]: | |
if st.button("π Reset"): | |
st.session_state.story = "" | |
st.session_state.concepts = [] | |
st.session_state.animation_path = None | |
st.session_state.active_tab = "story" | |
# Story creation tab | |
if st.session_state.active_tab == "story": | |
with st.container(): | |
st.header("π Create Your Story") | |
st.write("Write a short story (2-5 sentences) and I'll turn it into an animation!") | |
story = st.text_area( | |
"Your story:", | |
height=200, | |
placeholder="Once upon a time, a rabbit hopped 3 times to reach a carrot...", | |
value=st.session_state.story, | |
key="story_input" | |
) | |
if st.button("Create Animation!", use_container_width=True): | |
if len(story) < 10: | |
st.error("Your story needs to be at least 10 characters long!") | |
else: | |
st.session_state.story = story | |
with st.spinner("π§ Analyzing your story for coding concepts..."): | |
st.session_state.concepts = analyze_story(story) | |
with st.spinner("π¬ Creating your animation (this may take a moment)..."): | |
st.session_state.animation_path = generate_pygame_animation( | |
story, st.session_state.concepts | |
) | |
st.session_state.active_tab = "animation" | |
st.rerun() | |
# Show examples | |
st.subheader("β¨ Story Examples") | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
st.caption("Loop Example") | |
st.code('"A dragon breathes fire 5 times at the castle"', language="text") | |
with col2: | |
st.caption("Conditional Example") | |
st.code('"If it rains, the cat stays inside, else it goes out"', language="text") | |
with col3: | |
st.caption("Function Example") | |
st.code('"A wizard casts a spell to make flowers grow"', language="text") | |
# Animation tab | |
elif st.session_state.active_tab == "animation": | |
st.header("π¬ Your Story Animation") | |
if not st.session_state.animation_path: | |
st.warning("Please create a story first!") | |
st.session_state.active_tab = "story" | |
st.rerun() | |
# Display animation | |
st.markdown(f""" | |
<div class="animation-container"> | |
<h3 style="color: white; text-align: center;">"{st.session_state.story[:60]}{'...' if len(st.session_state.story) > 60 else ''}"</h3> | |
</div> | |
""", unsafe_allow_html=True) | |
try: | |
st.image(st.session_state.animation_path, use_container_width=True) | |
except Exception as e: | |
st.error(f"Couldn't display animation: {str(e)}") | |
story_image = create_story_image(st.session_state.story) | |
if story_image: | |
st.image(story_image, use_container_width=True) | |
st.success("β¨ Animation created successfully!") | |
st.caption("This animation was generated with Python code based on your story!") | |
if st.button("Reveal Coding Secrets!", use_container_width=True): | |
st.session_state.active_tab = "concepts" | |
st.rerun() | |
# Concepts tab | |
elif st.session_state.active_tab == "concepts": | |
st.header("π Coding Concepts in Your Story") | |
st.subheader("We secretly used these programming concepts:") | |
if not st.session_state.concepts: | |
st.warning("No concepts detected in your story! Try adding words like '3 times', 'if', or 'make'.") | |
else: | |
for concept in st.session_state.concepts: | |
if concept in CONCEPTS: | |
details = CONCEPTS[concept] | |
st.markdown(f""" | |
<div class="concept-card" style="border-left: 5px solid {details['color']};"> | |
<div style="display:flex; align-items:center; gap:15px;"> | |
<span style="font-size:36px;">{details['emoji']}</span> | |
<h3 style="color:{details['color']};">{details['name']}</h3> | |
</div> | |
<p>{details['description']}</p> | |
<pre style="background:#f0f0f0; padding:10px; border-radius:8px;">{details['example']}</pre> | |
</div> | |
""", unsafe_allow_html=True) | |
if st.button("See the Magic Code!", use_container_width=True): | |
st.session_state.active_tab = "code" | |
st.rerun() | |
# Code tab | |
elif st.session_state.active_tab == "code": | |
st.header("π» The Magic Code Behind Your Animation") | |
st.write("Here's the Python code that created your animation:") | |
# Sample code (in a real app, you would generate this based on the story) | |
sample_code = """ | |
# Story: {story} | |
import pygame | |
import numpy as np | |
import imageio | |
def create_animation(story): | |
# Setup PyGame | |
pygame.init() | |
WIDTH, HEIGHT = 800, 600 | |
screen = pygame.Surface((WIDTH, HEIGHT)) | |
# Animation parameters based on story | |
character = "{character}" | |
count = {count} | |
# Animation loop | |
frames = [] | |
for frame in range(120): | |
# Draw background, character, etc. | |
# ... animation code ... | |
# Capture frame | |
frame_data = pygame.surfarray.array3d(screen) | |
frames.append(frame_data) | |
# Save animation | |
imageio.mimsave('animation.gif', frames, fps=30) | |
create_animation("{story}") | |
""".format( | |
story=st.session_state.story[:50] + ("..." if len(st.session_state.story) > 50 else ""), | |
character=random.choice(list(CHARACTERS.keys())), | |
count=min([int(word) for word in st.session_state.story.split() if word.isdigit()] or [3]) | |
) | |
st.code(sample_code, language="python") | |
# Download button | |
st.download_button( | |
label="Download Animation Code", | |
data=sample_code, | |
file_name="story_animation.py", | |
mime="text/python", | |
use_container_width=True | |
) | |
st.write("You can run this code on your computer to create similar animations!") | |
if st.button("Create Another Story!", use_container_width=True): | |
st.session_state.active_tab = "story" | |
st.session_state.story = "" | |
st.rerun() | |
if __name__ == "__main__": | |
main() |