oussnaji's picture
Create app.py
27138e8 verified
import streamlit as st
import anthropic
import requests
import json
import base64
import plotly.graph_objects as go
from typing import Dict, Any
import time
import random
# Page Config
st.set_page_config(
page_title="NarrativeCraft: Immersive Story & Character Design Studio",
layout="wide",
initial_sidebar_state="expanded",
page_icon="πŸ“š"
)
# Custom CSS for professional dark theme
st.markdown("""
<style>
.main {background-color: #0e1117; color: #ffffff;}
.stProgress > div > div > div > div {background-color: #1f77b4;}
.character-card {
background-color: #1e1e1e;
padding: 1.5rem;
border-radius: 0.5rem;
margin: 1rem 0;
border: 1px solid #2e2e2e;
}
.section-title {
font-size: 1.5rem;
font-weight: bold;
margin: 1.5rem 0 1rem 0;
padding-bottom: 0.5rem;
border-bottom: 2px solid #2e2e2e;
}
.subsection-title {
font-size: 1.2rem;
font-weight: bold;
margin: 1rem 0;
color: #4e8cff;
}
.story-section {
background-color: #1e1e1e;
padding: 2rem;
border-radius: 0.5rem;
margin: 1.5rem 0;
border: 1px solid #2e2e2e;
}
.audio-player {
margin: 1rem 0;
padding: 1rem;
background-color: #2e2e2e;
border-radius: 0.5rem;
}
.analysis-card {
background-color: #2e2e2e;
padding: 1rem;
border-radius: 0.5rem;
margin: 0.5rem 0;
}
</style>
""", unsafe_allow_html=True)
# Initialize session state
if 'character' not in st.session_state:
st.session_state.character = None
if 'story_params' not in st.session_state:
st.session_state.story_params = None
if 'generated_content' not in st.session_state:
st.session_state.generated_content = None
if 'voice_data' not in st.session_state:
st.session_state.voice_data = None
# Constants
ARCHETYPES = {
"The Hero": "Brave, determined protagonist driven by a noble cause",
"The Mentor": "Wise guide with deep knowledge and experience",
"The Trickster": "Clever, unpredictable character who challenges conventions",
"The Sage": "Philosophical, thoughtful character seeking truth",
"The Rebel": "Independent spirit fighting against the system",
"The Caregiver": "Nurturing, protective character driven by compassion",
"The Creator": "Innovative, artistic character driven by vision",
"The Explorer": "Adventure-seeking character driven by curiosity",
"The Ruler": "Leadership-focused character seeking control",
"The Innocent": "Pure-hearted character maintaining optimism"
}
GENRES = [
"Epic Fantasy",
"Science Fiction",
"Dark Fantasy",
"Historical Fiction",
"Magical Realism",
"Contemporary Drama",
"Mystery/Thriller",
"Romance",
"Political Intrigue",
"Cyberpunk",
"Space Opera",
"Urban Fantasy",
"Gothic Horror",
"Adventure",
"Literary Fiction"
]
WRITING_STYLES = {
"Classical": "Elegant, formal prose with rich descriptions",
"Modern Minimalist": "Clean, precise language with impact",
"Lyrical": "Poetic, flowing prose with metaphorical depth",
"Gritty Realism": "Raw, direct style with stark honesty",
"Experimental": "Innovative structure and unique voice",
"Journalistic": "Clear, factual style with objectivity",
"Stream of Consciousness": "Free-flowing, internal narrative",
"Epic": "Grand, sweeping style with historical weight"
}
def create_character_section():
st.markdown('<p class="section-title">Character Design</p>', unsafe_allow_html=True)
col1, col2 = st.columns([2, 1])
with col1:
selected_archetype = st.selectbox(
"Choose Character Archetype",
list(ARCHETYPES.keys())
)
st.info(ARCHETYPES[selected_archetype])
age_range = st.select_slider(
"Age Range",
options=["Child", "Teen", "Young Adult", "Adult", "Middle-aged", "Elderly"],
value="Adult"
)
with col2:
st.markdown('<p class="subsection-title">Character Preview</p>', unsafe_allow_html=True)
st.markdown(f"**Archetype:** {selected_archetype}")
st.markdown(f"**Age:** {age_range}")
st.markdown('<p class="subsection-title">Personality Traits</p>', unsafe_allow_html=True)
trait_col1, trait_col2, trait_col3 = st.columns(3)
with trait_col1:
confidence = st.slider("Confidence", 1, 10, 5, help="Character's self-assurance level")
empathy = st.slider("Empathy", 1, 10, 5, help="Ability to understand others")
intelligence = st.slider("Intelligence", 1, 10, 5, help="Mental capability and wit")
with trait_col2:
courage = st.slider("Courage", 1, 10, 5, help="Bravery in face of adversity")
ambition = st.slider("Ambition", 1, 10, 5, help="Drive to achieve goals")
loyalty = st.slider("Loyalty", 1, 10, 5, help="Faithfulness to causes/people")
with trait_col3:
humor = st.slider("Humor", 1, 10, 5, help="Sense of humor and wit")
creativity = st.slider("Creativity", 1, 10, 5, help="Imaginative capability")
wisdom = st.slider("Wisdom", 1, 10, 5, help="Depth of understanding")
st.markdown('<p class="subsection-title">Voice & Speech</p>', unsafe_allow_html=True)
voice_col1, voice_col2 = st.columns(2)
with voice_col1:
voice_type = st.selectbox(
"Voice Quality",
["Deep", "Melodious", "Rough", "Soft", "Commanding", "Gentle", "Energetic"],
help="Primary characteristic of the character's voice"
)
accent = st.selectbox(
"Accent",
["Standard", "Regional", "Foreign", "Cultured", "Rustic"],
help="Character's accent or dialect"
)
with voice_col2:
speech_pattern = st.selectbox(
"Speech Pattern",
["Formal", "Casual", "Educated", "Street-wise", "Poetic", "Technical", "Noble"],
help="How the character typically speaks"
)
speech_pacing = st.select_slider(
"Speech Pace",
options=["Very Slow", "Slow", "Moderate", "Quick", "Very Quick"],
value="Moderate",
help="Speed and rhythm of speech"
)
st.markdown('<p class="subsection-title">Emotional Profile</p>', unsafe_allow_html=True)
emo_col1, emo_col2 = st.columns(2)
with emo_col1:
primary_emotion = st.selectbox(
"Primary Emotional State",
["Determined", "Serene", "Passionate", "Calculating", "Troubled", "Optimistic", "Reserved"]
)
emotional_stability = st.slider("Emotional Stability", 1, 10, 5)
with emo_col2:
emotional_expression = st.selectbox(
"Emotional Expression Style",
["Open", "Guarded", "Volatile", "Controlled", "Subtle", "Dramatic"]
)
emotional_depth = st.slider("Emotional Depth", 1, 10, 5)
return {
"archetype": selected_archetype,
"age_range": age_range,
"traits": {
"confidence": confidence,
"empathy": empathy,
"intelligence": intelligence,
"courage": courage,
"ambition": ambition,
"loyalty": loyalty,
"humor": humor,
"creativity": creativity,
"wisdom": wisdom
},
"voice": {
"type": voice_type,
"accent": accent,
"pattern": speech_pattern,
"pacing": speech_pacing
},
"emotional_profile": {
"primary_emotion": primary_emotion,
"stability": emotional_stability,
"expression": emotional_expression,
"depth": emotional_depth
}
}
def create_voice_preview(description: str, text: str) -> Dict:
"""Create voice preview using ElevenLabs API"""
url = 'https://api.elevenlabs.io/v1/text-to-voice/create-previews'
headers = {
'xi-api-key': st.secrets["ELEVENLABS_API_KEY"],
'Content-Type': 'application/json'
}
payload = {
'voice_description': description,
'text': text
}
try:
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
return response.json()
else:
st.error(f"Voice generation error: {response.status_code}")
st.write("Error details:", response.json())
return None
except Exception as e:
st.error(f"Error: {str(e)}")
return None
def display_character_profile(character: Dict, voice_data: Dict = None):
"""Display character profile with voice playback"""
if not character:
st.warning("No character data available")
return
try:
with st.container():
st.markdown('<div class="character-card">', unsafe_allow_html=True)
# Character Title
if character.get("archetype"):
st.markdown(f'<p class="section-title">{character["archetype"]}</p>',
unsafe_allow_html=True)
col1, col2 = st.columns([2, 1])
with col1:
if character.get("traits"):
st.markdown("### Personality Profile")
# Personality Traits Radar Chart
traits = character['traits']
fig = go.Figure(data=go.Scatterpolar(
r=[traits[key] for key in traits.keys()],
theta=list(traits.keys()),
fill='toself',
line=dict(color='#4e8cff')
))
fig.update_layout(
polar=dict(
radialaxis=dict(visible=True, range=[0, 10])),
showlegend=False,
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
font=dict(color='white')
)
st.plotly_chart(fig, use_container_width=True)
with col2:
st.markdown('<p class="subsection-title">Voice Sample</p>',
unsafe_allow_html=True)
if voice_data and 'previews' in voice_data and voice_data['previews']:
st.markdown('<div class="audio-player">', unsafe_allow_html=True)
audio_data = base64.b64decode(voice_data['previews'][0]['audio_base_64'])
st.audio(audio_data, format='audio/mp3')
st.markdown('</div>', unsafe_allow_html=True)
if character.get("voice"):
st.markdown('<p class="subsection-title">Core Traits</p>',
unsafe_allow_html=True)
st.markdown(f"**Age Range:** {character.get('age_range', 'N/A')}")
st.markdown(f"**Voice Type:** {character['voice'].get('type', 'N/A')}")
st.markdown(f"**Speech Pattern:** {character['voice'].get('pattern', 'N/A')}")
st.markdown('</div>', unsafe_allow_html=True)
except Exception as e:
st.error(f"Error displaying character profile: {str(e)}")
def create_story_parameters_section():
st.markdown('<p class="section-title">Story Configuration</p>', unsafe_allow_html=True)
# Main Story Parameters
col1, col2 = st.columns(2)
with col1:
genre = st.selectbox("Genre", GENRES)
writing_style = st.selectbox(
"Writing Style",
list(WRITING_STYLES.keys())
)
st.info(WRITING_STYLES[writing_style])
tone = st.select_slider(
"Story Tone",
options=["Light", "Hopeful", "Neutral", "Dark", "Gritty"],
value="Neutral"
)
with col2:
pacing = st.select_slider(
"Story Pacing",
options=["Slow & Thoughtful", "Balanced", "Fast & Intense"],
value="Balanced"
)
plot_complexity = st.slider("Plot Complexity", 1, 10, 5)
dialogue_focus = st.slider("Dialogue Emphasis", 1, 10, 5)
# Setting Details
st.markdown('<p class="subsection-title">Setting & Atmosphere</p>', unsafe_allow_html=True)
set_col1, set_col2 = st.columns(2)
with set_col1:
time_period = st.selectbox(
"Time Period",
["Ancient", "Medieval", "Renaissance", "Industrial", "Modern",
"Contemporary", "Near Future", "Far Future", "Multiple Eras"]
)
setting_type = st.selectbox(
"Setting Type",
["Urban", "Rural", "Wilderness", "Fantasy World", "Space",
"Underground", "Ocean", "Multiple Locations"]
)
with set_col2:
atmosphere = st.select_slider(
"Atmosphere",
options=["Mysterious", "Whimsical", "Tense", "Peaceful", "Epic", "Intimate"],
value="Tense"
)
realism_level = st.select_slider(
"Realism Level",
options=["Highly Realistic", "Balanced", "Fantastical"],
value="Balanced"
)
# Themes and Conflicts
st.markdown('<p class="subsection-title">Themes & Conflicts</p>', unsafe_allow_html=True)
theme_col1, theme_col2 = st.columns(2)
with theme_col1:
themes = st.multiselect(
"Main Themes",
["Redemption", "Power", "Love", "Justice", "Identity", "Change",
"Good vs Evil", "Order vs Chaos", "Faith", "Family"],
default=["Identity"]
)
conflict_type = st.selectbox(
"Primary Conflict",
["Person vs Person", "Person vs Nature", "Person vs Society",
"Person vs Self", "Person vs Technology", "Person vs Fate"]
)
with theme_col2:
moral_ambiguity = st.select_slider(
"Moral Ambiguity",
options=["Clear Good/Evil", "Some Gray Areas", "Morally Complex"],
value="Some Gray Areas"
)
theme_exploration = st.select_slider(
"Theme Exploration",
options=["Subtle", "Balanced", "Overt"],
value="Balanced"
)
return {
"main_params": {
"genre": genre,
"writing_style": writing_style,
"tone": tone,
"pacing": pacing,
"plot_complexity": plot_complexity,
"dialogue_focus": dialogue_focus
},
"setting": {
"time_period": time_period,
"type": setting_type,
"atmosphere": atmosphere,
"realism": realism_level
},
"themes": {
"main_themes": themes,
"conflict": conflict_type,
"moral_ambiguity": moral_ambiguity,
"exploration": theme_exploration
}
}
def display_story_preview(story_params: Dict, generated_content: Dict = None):
"""Display generated story content"""
if not generated_content:
st.info("Generate preview to see content")
return
st.markdown('<div class="story-section">', unsafe_allow_html=True)
# Tabs for content sections
tabs = st.tabs(["Story", "Analysis"])
with tabs[0]:
# Prologue
st.markdown('<p class="subsection-title" style="color: #4e8cff;">Prologue</p>',
unsafe_allow_html=True)
st.markdown(generated_content.get('prologue', ''))
# Character Analysis
if generated_content.get('character_analysis'):
st.markdown('<p class="subsection-title" style="color: #4e8cff;">Character Analysis</p>',
unsafe_allow_html=True)
st.markdown(generated_content['character_analysis'])
# Scene Preview
if generated_content.get('scene_preview'):
st.markdown('<p class="subsection-title" style="color: #4e8cff;">Scene Preview</p>',
unsafe_allow_html=True)
st.markdown(generated_content['scene_preview'])
with tabs[1]:
col1, col2 = st.columns(2)
with col1:
# Themes
st.markdown('<p class="subsection-title" style="color: #4e8cff;">Themes & Motifs</p>',
unsafe_allow_html=True)
themes = generated_content.get('analysis', {}).get('themes', [])
if themes:
for theme in themes:
st.markdown(f"β€’ {theme}")
else:
st.info("No themes specified")
with col2:
# Style Analysis
st.markdown('<p class="subsection-title" style="color: #4e8cff;">Style Analysis</p>',
unsafe_allow_html=True)
style = generated_content.get('analysis', {}).get('style', {})
if style:
for key, value in style.items():
st.markdown(f"**{key}:**")
st.markdown(value)
else:
st.info("No style analysis available")
st.markdown('</div>', unsafe_allow_html=True)
def generate_story_content(character: Dict, story_params: Dict) -> Dict:
"""Generate story content using Claude"""
client = anthropic.Anthropic(api_key=st.secrets["ANTHROPIC_API_KEY"])
prompt = f"""Create a compelling and human-like story preview based on the following character and parameters:
Character Details:
{json.dumps(character, indent=2)}
Story Parameters:
{json.dumps(story_params, indent=2)}
Please provide:
1. Prologue (2-3 engaging paragraphs)
- Set the tone and atmosphere
- Introduce the world/setting
- Create intrigue
2. Character Analysis
- Deep psychological insights
- Key motivations and conflicts
- Unique traits and quirks
3. Sample Scene
- Show character in action
- Demonstrate personality
- Include meaningful dialogue
4. Thematic Analysis
- Provide 3-4 major themes
- Explain their significance
- Show their manifestation
5. Style Elements
- Discuss narrative approach
- Note key stylistic choices
- Highlight unique features
Make the writing feel natural and professional, avoiding any AI-like patterns. Focus on depth and authenticity."""
try:
response = client.messages.create(
max_tokens=4096,
model="claude-3-5-sonnet-latest",
messages=[{"role": "user", "content": prompt}]
)
content = response.content[0].text
# Parse the content into structured sections
sections = parse_generated_content(content)
return sections
except Exception as e:
st.error(f"Error generating content: {str(e)}")
return None
def parse_generated_content(content: str) -> Dict:
"""Parse Claude's response into structured sections"""
sections = {
"prologue": "",
"character_analysis": "",
"scene_preview": "",
"analysis": {
"themes": [],
"style": {}
}
}
try:
current_section = None
buffer = []
for line in content.split('\n'):
line = line.strip()
if not line:
continue
lower_line = line.lower()
# Section detection
if "prologue" in lower_line and len(line) < 30:
if buffer and current_section:
sections[current_section] = '\n'.join(buffer).strip()
current_section = "prologue"
buffer = []
continue
elif "character analysis" in lower_line and len(line) < 30:
if buffer and current_section:
sections[current_section] = '\n'.join(buffer).strip()
current_section = "character_analysis"
buffer = []
continue
elif "sample scene" in lower_line and len(line) < 30:
if buffer and current_section:
sections[current_section] = '\n'.join(buffer).strip()
current_section = "scene_preview"
buffer = []
continue
elif ("theme" in lower_line or "themes" in lower_line) and len(line) < 30:
if buffer and current_section:
sections[current_section] = '\n'.join(buffer).strip()
current_section = "themes"
buffer = []
continue
elif "style" in lower_line and len(line) < 30:
if buffer and current_section:
sections[current_section] = '\n'.join(buffer).strip()
current_section = "style"
buffer = []
continue
# Content processing
if current_section:
if current_section == "themes":
if line.startswith(('β€’', '-', '*')):
clean_line = line.lstrip('β€’-* ').strip()
sections['analysis']['themes'].append(clean_line)
elif line:
buffer.append(line)
elif current_section == "style":
if ':' in line:
key, value = line.split(':', 1)
key = key.strip().strip('0123456789.- ').strip()
sections['analysis']['style'][key] = value.strip()
elif line:
buffer.append(line)
else:
buffer.append(line)
# Process any remaining buffer
if buffer and current_section:
if current_section in ["themes", "style"]:
# Process remaining theme/style buffer if needed
pass
else:
sections[current_section] = '\n'.join(buffer).strip()
return sections
except Exception as e:
st.error(f"Error parsing content: {str(e)}")
return None
def main():
st.title("NarrativeCraft: Immersive Story & Character Design Studio")
st.markdown("#### Created by Oussama Naji")
with st.sidebar:
st.markdown("### Creation Process")
current_step = st.radio(
"Select Step:",
["Character Creation", "Story Configuration", "Generate Preview"]
)
try:
if current_step == "Character Creation":
character = create_character_section()
if st.button("Save Character", type="primary"):
st.session_state.character = character
st.success("Character saved! Proceed to Story Configuration.")
elif current_step == "Story Configuration":
if 'character' not in st.session_state:
st.warning("Please create a character first!")
return
story_params = create_story_parameters_section()
if st.button("Save Story Parameters", type="primary"):
st.session_state.story_params = story_params
st.success("Story parameters saved! Proceed to Preview Generation.")
else: # Generate Preview
if 'character' not in st.session_state or 'story_params' not in st.session_state:
st.warning("Please complete character and story configuration first!")
return
if st.button("Generate Preview", type="primary"):
with st.spinner("🎨 Crafting your story..."):
progress_bar = st.progress(0)
try:
# Generate story content
progress_bar.progress(25)
st.info("πŸ€– Generating story content...")
st.session_state.generated_content = generate_story_content(
st.session_state.character,
st.session_state.story_params
)
if not st.session_state.generated_content:
st.error("Failed to generate story content")
return
# Generate voice
progress_bar.progress(50)
st.info("🎀 Creating character voice...")
voice_desc = f"A {st.session_state.character['voice']['type']} voice with {st.session_state.character['voice']['pattern']} speech pattern"
sample_text = "I stand here before you, ready to share my story. Each word carries the weight of my experiences, the essence of who I am."
st.session_state.voice_data = create_voice_preview(voice_desc, sample_text)
progress_bar.progress(75)
st.info("✨ Finalizing preview...")
# Display results
if st.session_state.character:
display_character_profile(
st.session_state.character,
st.session_state.voice_data
)
if st.session_state.story_params and st.session_state.generated_content:
display_story_preview(
st.session_state.story_params,
st.session_state.generated_content
)
progress_bar.progress(100)
st.success("βœ… Preview generated successfully!")
except Exception as e:
st.error(f"An error occurred: {str(e)}")
return
# Display existing content if available
elif all(key in st.session_state for key in ['character', 'generated_content']):
display_character_profile(
st.session_state.character,
st.session_state.voice_data
)
display_story_preview(
st.session_state.story_params,
st.session_state.generated_content
)
# Reset button
if 'generated_content' in st.session_state:
if st.button("Start Over"):
for key in ['character', 'story_params', 'generated_content', 'voice_data']:
if key in st.session_state:
del st.session_state[key]
st.rerun()
except Exception as e:
st.error(f"An error occurred in main: {str(e)}")
if __name__ == "__main__":
main()