File size: 20,666 Bytes
de2fdbb a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff de2fdbb a7374a3 de2fdbb a7374a3 de2fdbb a7374a3 32b08ff a7374a3 32b08ff de2fdbb a7374a3 32b08ff a7374a3 32b08ff de2fdbb 32b08ff a7374a3 32b08ff a7374a3 de2fdbb 32b08ff a7374a3 32b08ff de2fdbb a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff a7374a3 32b08ff de2fdbb a7374a3 de2fdbb a7374a3 32b08ff a7374a3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# app.py
import streamlit as st
from core.gemini_handler import GeminiHandler
from core.visual_engine import VisualEngine # Your updated VisualEngine
from core.prompt_engineering import (
create_story_breakdown_prompt,
create_image_prompt_from_scene_data, # Generates the TEXT prompt for AI/placeholder
create_scene_regeneration_prompt,
create_visual_regeneration_prompt # Regenerates the TEXT prompt
)
import os
# import json # Only if you need to st.json for debugging, not essential for core logic
# --- Configuration & Initialization ---
st.set_page_config(page_title="CineGen AI Pro", layout="wide", initial_sidebar_state="expanded")
# --- Global State Variables (Using session state for persistence) ---
if 'GEMINI_API_KEY' not in st.session_state:
try:
st.session_state.GEMINI_API_KEY = st.secrets["GEMINI_API_KEY"]
except KeyError: # For local dev if secrets aren't set yet
if "GEMINI_API_KEY" in os.environ:
st.session_state.GEMINI_API_KEY = os.environ["GEMINI_API_KEY"]
else:
st.error("GEMINI_API_KEY not found in secrets or environment variables. Please add it.")
st.stop()
except Exception as e: # Catch any other secrets-related errors
st.error(f"Error loading secrets: {e}")
st.stop()
if 'gemini_handler' not in st.session_state:
st.session_state.gemini_handler = GeminiHandler(api_key=st.session_state.GEMINI_API_KEY)
if 'visual_engine' not in st.session_state:
# Ensure VisualEngine is initialized (it will print font loading status)
st.session_state.visual_engine = VisualEngine(output_dir="temp_cinegen_media")
# Story and generated content
if 'story_scenes' not in st.session_state: # List of scene dicts
st.session_state.story_scenes = []
if 'scene_image_prompts' not in st.session_state: # List of textual image prompts
st.session_state.scene_image_prompts = []
if 'generated_images_paths' not in st.session_state: # List of file paths for generated visuals
st.session_state.generated_images_paths = []
if 'video_path' not in st.session_state:
st.session_state.video_path = None
# For Character Consistency (placeholders)
if 'character_definitions' not in st.session_state:
st.session_state.character_definitions = {}
# For Style Transfer (placeholders)
if 'style_reference_description' not in st.session_state:
st.session_state.style_reference_description = None
# --- Helper Functions ---
def initialize_new_story():
st.session_state.story_scenes = []
st.session_state.scene_image_prompts = []
st.session_state.generated_images_paths = []
st.session_state.video_path = None
# Clear old media files if any (optional, good for space management on persistent storage)
# For Hugging Face Spaces, temp files are usually cleared on restart/rebuild anyway.
# if os.path.exists(st.session_state.visual_engine.output_dir):
# for f in os.listdir(st.session_state.visual_engine.output_dir):
# os.remove(os.path.join(st.session_state.visual_engine.output_dir, f))
def generate_visual_for_scene_wrapper(scene_index, scene_data, is_regeneration=False, version_count=1):
"""
Wrapper to generate the textual image prompt and then the actual visual.
Updates session state for prompts and image paths.
"""
st.info(f"Generating visual concept for Scene {scene_data.get('scene_number', scene_index + 1)}...")
# 1. Get/Regenerate the detailed image prompt TEXT
if is_regeneration and scene_index < len(st.session_state.scene_image_prompts):
# For regeneration, we assume the prompt text is already updated in session state
# (e.g., by a call to create_visual_regeneration_prompt and Gemini)
textual_image_prompt = st.session_state.scene_image_prompts[scene_index]
else:
textual_image_prompt = create_image_prompt_from_scene_data(
scene_data,
st.session_state.character_definitions,
st.session_state.style_reference_description
)
# Optional: Call Gemini here to refine this initial prompt if desired
# textual_image_prompt = st.session_state.gemini_handler.generate_image_prompt(textual_image_prompt)
if not textual_image_prompt:
st.error(f"Failed to create textual image prompt for Scene {scene_data.get('scene_number', scene_index + 1)}.")
return False
# Update session state for the textual prompt
if scene_index < len(st.session_state.scene_image_prompts):
st.session_state.scene_image_prompts[scene_index] = textual_image_prompt
else:
# Ensure list is long enough before appending (shouldn't happen if initialized correctly)
while len(st.session_state.scene_image_prompts) <= scene_index:
st.session_state.scene_image_prompts.append("")
st.session_state.scene_image_prompts[scene_index] = textual_image_prompt
# 2. Generate the actual visual (AI or placeholder) using the textual prompt
image_filename = f"scene_{scene_data.get('scene_number', scene_index + 1)}_visual_v{version_count}.png"
generated_image_path = st.session_state.visual_engine.generate_image_visual(
textual_image_prompt,
image_filename
)
if generated_image_path and os.path.exists(generated_image_path):
st.success(f"Visual concept for Scene {scene_data.get('scene_number', scene_index + 1)} generated: {os.path.basename(generated_image_path)}")
if scene_index < len(st.session_state.generated_images_paths):
st.session_state.generated_images_paths[scene_index] = generated_image_path
else:
while len(st.session_state.generated_images_paths) <= scene_index:
st.session_state.generated_images_paths.append(None)
st.session_state.generated_images_paths[scene_index] = generated_image_path
return True
else:
st.error(f"Visual generation failed for Scene {scene_data.get('scene_number', scene_index + 1)} (path: {generated_image_path}).")
if scene_index < len(st.session_state.generated_images_paths):
st.session_state.generated_images_paths[scene_index] = None
else:
while len(st.session_state.generated_images_paths) <= scene_index:
st.session_state.generated_images_paths.append(None)
st.session_state.generated_images_paths[scene_index] = None
return False
# --- UI Sidebar ---
with st.sidebar:
st.title("π¬ CineGen AI Pro")
st.markdown("### Creative Controls")
user_idea = st.text_area("Enter your core story idea:", "A detective in a cyberpunk city investigates a rogue AI that believes it's the next step in evolution.", height=100, key="user_idea_input")
genre = st.selectbox("Genre:", ["Sci-Fi", "Fantasy", "Noir", "Thriller", "Drama", "Horror"], index=2, key="genre_select")
mood = st.selectbox("Mood:", ["Suspenseful", "Mysterious", "Gritty", "Epic", "Dark", "Hopeful"], index=2, key="mood_select")
num_scenes_val = st.slider("Number of Scenes:", 1, 5, 3, key="num_scenes_slider") # Max 5 for now
if st.button("β¨ Generate Full Story Concept", type="primary", key="generate_full_story_btn"):
initialize_new_story()
if not user_idea.strip():
st.warning("Please enter a story idea.")
else:
with st.spinner("Phase 1: Gemini is drafting the script & scene breakdown... π"):
story_prompt_text = create_story_breakdown_prompt(user_idea, genre, mood, num_scenes_val)
try:
st.session_state.story_scenes = st.session_state.gemini_handler.generate_story_breakdown(story_prompt_text)
st.toast("Script breakdown complete!", icon="β
")
# Initialize placeholders for prompts and images based on number of scenes
num_actual_scenes = len(st.session_state.story_scenes)
st.session_state.scene_image_prompts = [""] * num_actual_scenes
st.session_state.generated_images_paths = [None] * num_actual_scenes
except Exception as e:
st.error(f"Failed to generate story breakdown: {e}")
st.session_state.story_scenes = []
if st.session_state.story_scenes:
with st.spinner("Phase 2: Generating initial visual concepts... πΌοΈ"):
success_count = 0
for i, scene_data_loop_var in enumerate(st.session_state.story_scenes):
if generate_visual_for_scene_wrapper(i, scene_data_loop_var, version_count=1):
success_count +=1
if success_count == len(st.session_state.story_scenes):
st.toast("Initial visual concepts generated!", icon="πΌοΈ")
else:
st.warning(f"{success_count}/{len(st.session_state.story_scenes)} visual concepts generated. Some may have failed.")
st.markdown("---")
st.markdown("### Advanced Options (Conceptual)")
with st.expander("Character Consistency", expanded=False):
char_name_input = st.text_input("Character Name (e.g., Eva)", key="char_name")
char_desc_input = st.text_area("Character Description (for visual consistency)", key="char_desc", height=80)
if st.button("Add/Update Character", key="add_char_btn"):
if char_name_input and char_desc_input:
st.session_state.character_definitions[char_name_input] = char_desc_input
st.success(f"Character '{char_name_input}' defined.")
else:
st.warning("Please provide both name and description.")
if st.session_state.character_definitions:
st.write("Defined Characters:")
for char, desc in st.session_state.character_definitions.items():
st.caption(f"**{char}:** {desc}")
with st.expander("Style Transfer", expanded=False):
style_ref_text = st.text_area("Describe Style (e.g., 'Van Gogh inspired, swirling brushstrokes')", key="style_text_input", height=80)
if st.button("Apply Textual Style", key="apply_style_btn"):
st.session_state.style_reference_description = style_ref_text
st.success("Style reference applied. Re-generate visuals or full story to see changes.")
# --- Main Content Area ---
st.header("π Cinematic Storyboard")
if not st.session_state.story_scenes:
st.info("Enter your idea in the sidebar and click 'Generate Full Story Concept' to begin.")
else:
for i, scene_data_display in enumerate(st.session_state.story_scenes):
scene_num_display = scene_data_display.get('scene_number', i + 1)
# Use a unique key part for expanders if scene numbers can repeat or change
expander_key_part = scene_data_display.get('key_action', f"scene{i}")[:20].replace(" ", "")
st.subheader(f"Scene {scene_num_display}: {scene_data_display.get('emotional_beat', 'Untitled Scene')}")
col1, col2 = st.columns([2, 3])
with col1: # Scene Details
with st.expander("Scene Details", expanded=True):
st.markdown(f"**Setting:** {scene_data_display.get('setting_description', 'N/A')}")
st.markdown(f"**Characters:** {', '.join(scene_data_display.get('characters_involved', []))}")
st.markdown(f"**Key Action:** {scene_data_display.get('key_action', 'N/A')}")
st.markdown(f"**Dialogue Snippet:** `\"{scene_data_display.get('dialogue_snippet', '...')}\"`")
st.markdown(f"**Visual Style:** {scene_data_display.get('visual_style_suggestion', 'N/A')}")
st.markdown(f"**Camera:** {scene_data_display.get('camera_angle_suggestion', 'N/A')}")
if i < len(st.session_state.scene_image_prompts) and st.session_state.scene_image_prompts[i]:
with st.popover("View Image Prompt Text"):
st.markdown(f"**Textual Prompt for Image Generation:**")
st.code(st.session_state.scene_image_prompts[i], language='text')
with col2: # Image and Interactive Editing
if i < len(st.session_state.generated_images_paths) and \
st.session_state.generated_images_paths[i] and \
os.path.exists(st.session_state.generated_images_paths[i]):
st.image(st.session_state.generated_images_paths[i], caption=f"Visual Concept for Scene {scene_num_display}")
else:
if st.session_state.story_scenes: # Only show if story generation was attempted
st.warning("Visual concept not generated or path is invalid for this scene.")
# Interactive Storyboarding UI using unique keys for each scene's widgets
popover_script_key = f"pop_script_{expander_key_part}_{i}"
popover_visual_key = f"pop_visual_{expander_key_part}_{i}"
with st.popover(f"βοΈ Edit Scene {scene_num_display} Script", key=popover_script_key):
feedback_script = st.text_area("Describe changes to script details:",
key=f"script_feedback_{expander_key_part}_{i}", height=100)
if st.button(f"π Regenerate Scene {scene_num_display} Script", key=f"regen_script_btn_{expander_key_part}_{i}"):
if feedback_script:
with st.spinner(f"Gemini is rewriting Scene {scene_num_display}..."):
regen_prompt = create_scene_regeneration_prompt(
scene_data_display, feedback_script, st.session_state.story_scenes
)
try:
updated_scene_data = st.session_state.gemini_handler.regenerate_scene_script_details(regen_prompt)
st.session_state.story_scenes[i] = updated_scene_data
st.toast(f"Scene {scene_num_display} script updated!", icon="βοΈ")
# Regenerate visuals for this updated scene with new script details
# Increment version for the image filename
current_version = 1
if st.session_state.generated_images_paths[i]:
try: # Try to extract version from existing filename
base, ext = os.path.splitext(os.path.basename(st.session_state.generated_images_paths[i]))
if '_v' in base:
current_version = int(base.split('_v')[-1]) + 1
except ValueError: pass # Keep current_version as 1 if parsing fails
generate_visual_for_scene_wrapper(i, updated_scene_data, is_regeneration=True, version_count=current_version)
st.rerun()
except Exception as e:
st.error(f"Error regenerating scene script: {e}")
else:
st.warning("Please provide feedback for script regeneration.")
with st.popover(f"π¨ Edit Scene {scene_num_display} Visuals", key=popover_visual_key):
current_prompt_for_edit = st.session_state.scene_image_prompts[i] if i < len(st.session_state.scene_image_prompts) else "No prompt yet."
st.caption("Current Image Prompt Text:")
st.code(current_prompt_for_edit, language='text')
feedback_visual = st.text_area("Describe visual changes to apply to the prompt:",
key=f"visual_feedback_{expander_key_part}_{i}", height=100)
if st.button(f"π Regenerate Scene {scene_num_display} Visuals", key=f"regen_visual_btn_{expander_key_part}_{i}"):
if feedback_visual:
with st.spinner(f"Refining image prompt for Scene {scene_num_display}..."):
# This creates a prompt FOR GEMINI to refine the image prompt
prompt_refinement_request = create_visual_regeneration_prompt(
current_prompt_for_edit,
feedback_visual,
scene_data_display
)
try:
# Gemini refines the textual image prompt
refined_textual_image_prompt = st.session_state.gemini_handler.regenerate_image_prompt_from_feedback(prompt_refinement_request)
st.session_state.scene_image_prompts[i] = refined_textual_image_prompt
st.toast(f"Image prompt for Scene {scene_num_display} refined!", icon="π‘")
# Now generate the new visual using the refined prompt
current_version = 1
if st.session_state.generated_images_paths[i]:
try:
base, ext = os.path.splitext(os.path.basename(st.session_state.generated_images_paths[i]))
if '_v' in base:
current_version = int(base.split('_v')[-1]) + 1
except ValueError: pass
generate_visual_for_scene_wrapper(i, scene_data_display, is_regeneration=True, version_count=current_version)
st.rerun()
except Exception as e:
st.error(f"Error refining image prompt or regenerating visual: {e}")
else:
st.warning("Please provide feedback for visual regeneration.")
st.markdown("---")
# Video Generation Button
if st.session_state.story_scenes and any(p for p in st.session_state.generated_images_paths): # Check if any image exists
if st.button("π¬ Assemble Animatic Video", key="assemble_video_btn", type="primary"):
with st.spinner("Assembling video... This might take a moment."):
valid_image_paths_for_video = [p for p in st.session_state.generated_images_paths if p and os.path.exists(p)]
if valid_image_paths_for_video:
st.session_state.video_path = st.session_state.visual_engine.create_video_from_images(
valid_image_paths_for_video,
output_filename="cinegen_pro_animatic.mp4",
duration_per_image=3
)
if st.session_state.video_path and os.path.exists(st.session_state.video_path):
st.toast("Animatic video assembled!", icon="ποΈ")
st.balloons()
else:
st.error("Video assembly failed. Check logs for details.")
else:
st.error("No valid images found to assemble video. Please generate visuals for scenes first.")
elif st.session_state.story_scenes: # Story exists but no images yet
st.warning("Generate visuals for scenes before assembling the video.")
if st.session_state.video_path and os.path.exists(st.session_state.video_path):
st.header("π¬ Generated Animatic Video")
try:
with open(st.session_state.video_path, 'rb') as video_file:
video_bytes = video_file.read()
st.video(video_bytes)
st.markdown(f"Video saved at: `{st.session_state.video_path}` (within the Space's file system)")
# Provide a download button for the video
with open(st.session_state.video_path, "rb") as fp_download:
st.download_button(
label="Download Animatic Video",
data=fp_download,
file_name=os.path.basename(st.session_state.video_path),
mime="video/mp4"
)
except Exception as e:
st.error(f"Error displaying or preparing video for download: {e}")
# --- Footer or additional info ---
st.sidebar.markdown("---")
st.sidebar.info("CineGen AI by [Your Name/Company]. Powered by Gemini & Streamlit.") |