mgbam commited on
Commit
71de144
Β·
verified Β·
1 Parent(s): 4cb150c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +138 -192
app.py CHANGED
@@ -1,10 +1,10 @@
1
- # app.py (Streamlit version for StoryVerse Weaver)
2
  import streamlit as st # FIRST STREAMLIT IMPORT
3
- from PIL import Image, ImageDraw, ImageFont # For creating placeholder images
4
  import os
5
  import time
6
  import random
7
- import traceback # For better error display
8
 
9
  # --- Page Configuration (MUST BE THE VERY FIRST STREAMLIT COMMAND) ---
10
  st.set_page_config(
@@ -15,15 +15,6 @@ st.set_page_config(
15
  )
16
  # --- END OF PAGE CONFIG ---
17
 
18
- # --- Add project root to sys.path if core modules are in a subdirectory ---
19
- # This might be needed depending on your exact file structure when running locally.
20
- # For Hugging Face Spaces, if 'core' and 'prompts' are at the same level as app.py,
21
- # direct imports usually work.
22
- # import sys
23
- # SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
24
- # sys.path.append(os.path.dirname(SCRIPT_DIR)) # If app.py is in a subdir like 'src'
25
- # sys.path.append(SCRIPT_DIR) # If core/prompts are subdirs of where app.py is
26
-
27
  # --- Core Logic Imports (NOW AFTER st.set_page_config) ---
28
  from core.llm_services import initialize_text_llms, is_gemini_text_ready, is_hf_text_ready, generate_text_gemini, generate_text_hf, LLMTextResponse
29
  from core.image_services import initialize_image_llms, is_dalle_ready, is_hf_image_api_ready, generate_image_dalle, generate_image_hf_model, ImageGenResponse
@@ -88,67 +79,33 @@ streamlit_omega_css = """
88
  <style>
89
  body { color: #D0D0E0; background-color: #0F0F1A; font-family: 'Lexend Deca', sans-serif;}
90
  .stApp { background-color: #0F0F1A; }
91
- h1, h2, h3, h4, h5, h6 { color: #C080F0; }
92
- /* Main containers */
93
- .main .block-container {
94
- padding-top: 2rem; padding-bottom: 2rem; padding-left: 2rem; padding-right: 2rem;
95
- max-width: 1400px; margin: auto;
96
- background-color: #1A1A2E; /* Panel background for main content area */
97
- border-radius: 15px;
98
- box-shadow: 0 8px 24px rgba(0,0,0,0.15);
99
- }
100
- /* Sidebar styling */
101
- [data-testid="stSidebar"] {
102
- background-color: #131325; /* Slightly different dark for sidebar */
103
- border-right: 1px solid #2A2A4A;
104
- }
105
- [data-testid="stSidebar"] .stMarkdown h3 { /* Sidebar headers */
106
- color: #D0D0FF !important;
107
- font-size: 1.5em;
108
- border-bottom: 2px solid #7F00FF;
109
- padding-bottom: 5px;
110
- }
111
- /* Input elements */
112
- .stTextInput > div > div > input, .stTextArea > div > div > textarea,
113
- .stSelectbox > div > div > div[data-baseweb="select"] > div,
114
- div[data-baseweb="input"] > input /* For text_input */
115
- {
116
- background-color: #2A2A4A !important; color: #E0E0FF !important;
117
- border: 1px solid #4A4A6A !important; border-radius: 8px !important;
118
- }
119
- /* Buttons */
120
  .stButton > button {
121
  background: linear-gradient(135deg, #7F00FF 0%, #E100FF 100%) !important;
122
  color: white !important; border: none !important; border-radius: 8px !important;
123
  padding: 0.7em 1.3em !important; font-weight: 600 !important;
124
  box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important;
125
- transition: all 0.2s ease-in-out;
126
- width: 100%; /* Make form submit button full width */
127
  }
128
  .stButton > button:hover { transform: scale(1.03) translateY(-1px); box-shadow: 0 8px 16px rgba(127,0,255,0.3) !important; }
129
  .stButton > button:disabled { background: #4A4A6A !important; color: #8080A0 !important; cursor: not-allowed; }
130
-
131
- /* Secondary button style (if you add more buttons) */
132
- /* You might need to assign a class and target it if Streamlit doesn't have variants like Gradio */
133
- button[kind="secondary"] { /* Conceptual, Streamlit buttons don't have 'kind' directly in CSS */
134
- background-color: #4A4A6A !important; color: #E0E0FF !important;
135
- }
136
-
137
- /* Image display */
138
  .stImage > img { border-radius: 12px; box-shadow: 0 6px 15px rgba(0,0,0,0.25); max-height: 550px; margin: auto; display: block;}
139
- /* Expander styling */
140
  .stExpander { background-color: #161628; border: 1px solid #2A2A4A; border-radius: 12px; margin-bottom: 1em;}
141
  .stExpander header { font-size: 1.1em; font-weight: 500; color: #D0D0FF;}
142
- /* Note/Alert styling */
143
  .important-note { background-color: rgba(127,0,255,0.1); border-left: 5px solid #7F00FF; padding: 15px; margin-bottom:20px; color: #E0E0FF; border-radius: 6px;}
144
- /* Custom class for status messages */
145
  .status-message { padding: 10px; border-radius: 6px; margin-top: 10px; text-align: center; font-weight: 500; }
146
  .status-success { background-color: #104010; color: #B0FFB0; border: 1px solid #208020; }
147
  .status-error { background-color: #401010; color: #FFB0B0; border: 1px solid #802020; }
148
  .status-processing { background-color: #102040; color: #B0D0FF; border: 1px solid #204080; }
149
- /* Gallery columns */
150
  .gallery-col img { border-radius: 8px; box-shadow: 0 2px 6px rgba(0,0,0,0.15); margin-bottom: 5px;}
151
- .gallery-col .stCaption { font-size: 0.85em; text-align: center; }
152
  </style>
153
  """
154
  st.markdown(streamlit_omega_css, unsafe_allow_html=True)
@@ -166,199 +123,186 @@ def create_placeholder_image_st(text="Processing...", size=(512, 512), color="#2
166
  draw.text(((size[0]-tw)/2, (size[1]-th)/2), text, font=font, fill=text_color); return img
167
 
168
  # --- Initialize Session State ---
169
- if 'story_object' not in st.session_state:
170
- st.session_state.story_object = Story()
171
- if 'current_log' not in st.session_state:
172
- st.session_state.current_log = ["Welcome to StoryVerse Omega (Streamlit Edition)!"]
173
- if 'latest_scene_image_pil' not in st.session_state:
174
- st.session_state.latest_scene_image_pil = None
175
- if 'latest_scene_narrative' not in st.session_state:
176
- st.session_state.latest_scene_narrative = "## ✨ A New Story Begins ✨\nDescribe your first scene idea in the panel on the left!"
177
- if 'status_message' not in st.session_state:
178
- st.session_state.status_message = {"text": "Ready to weave your first masterpiece!", "type": "processing"}
179
- if 'processing_scene' not in st.session_state:
180
- st.session_state.processing_scene = False
181
  if 'form_scene_prompt' not in st.session_state: st.session_state.form_scene_prompt = ""
182
  if 'form_image_style' not in st.session_state: st.session_state.form_image_style = "Default (Cinematic Realism)"
183
  if 'form_artist_style' not in st.session_state: st.session_state.form_artist_style = ""
184
 
185
-
186
- # --- Main App UI & Logic ---
187
- st.markdown("<div align='center'><h1>✨ StoryVerse Omega ✨</h1>\n<h3>Craft Immersive Multimodal Worlds with AI</h3></div>", unsafe_allow_html=True)
188
- st.markdown("<div class='important-note'><strong>Welcome, Worldsmith!</strong> Describe your vision, choose your style, and let Omega help you weave captivating scenes with narrative and imagery. Ensure API keys (<code>STORYVERSE_...</code>) are correctly set in your environment/secrets!</div>", unsafe_allow_html=True)
189
-
190
 
191
  # --- Sidebar for Inputs & Configuration ---
192
  with st.sidebar:
193
  st.markdown("### πŸ’‘ **Craft Your Scene**")
194
-
195
- with st.form("scene_input_form"):
196
- scene_prompt_text_input = st.text_area( # Unique key for widget
197
- "Scene Vision (Description, Dialogue, Action):",
198
- height=200,
199
- value=st.session_state.form_scene_prompt,
200
- placeholder="e.g., Amidst swirling cosmic dust..."
201
- )
202
  st.markdown("#### 🎨 Visual Style")
203
  col_style1, col_style2 = st.columns(2)
204
  with col_style1:
205
- image_style_dropdown_input = st.selectbox("Style Preset:", options=["Default (Cinematic Realism)"] + sorted(list(STYLE_PRESETS.keys())),
206
- index= (["Default (Cinematic Realism)"] + sorted(list(STYLE_PRESETS.keys()))).index(st.session_state.form_image_style) if st.session_state.form_image_style in (["Default (Cinematic Realism)"] + sorted(list(STYLE_PRESETS.keys()))) else 0,
207
- key="sel_image_style")
208
  with col_style2:
209
- artist_style_text_input = st.text_input("Artistic Inspiration (Optional):", placeholder="e.g., Moebius", value=st.session_state.form_artist_style, key="sel_artist_style")
 
210
 
211
- negative_prompt_text_input = st.text_area("Exclude from Image (Negative Prompt):", value=COMMON_NEGATIVE_PROMPTS, height=100, key="sel_negative_prompt")
212
 
213
  with st.expander("βš™οΈ Advanced AI Configuration", expanded=False):
214
- text_model_key_input = st.selectbox("Narrative AI Engine:", options=list(TEXT_MODELS.keys()),
215
- index=list(TEXT_MODELS.keys()).index(UI_DEFAULT_TEXT_MODEL_KEY) if UI_DEFAULT_TEXT_MODEL_KEY in TEXT_MODELS else 0,
216
- key="sel_text_model")
217
- image_provider_key_input = st.selectbox("Visual AI Engine:", options=list(IMAGE_PROVIDERS.keys()),
218
- index=list(IMAGE_PROVIDERS.keys()).index(UI_DEFAULT_IMAGE_PROVIDER_KEY) if UI_DEFAULT_IMAGE_PROVIDER_KEY in IMAGE_PROVIDERS else 0,
219
- key="sel_image_provider")
220
- narrative_length_input = st.selectbox("Narrative Detail:", options=["Short (1 paragraph)", "Medium (2-3 paragraphs)", "Detailed (4+ paragraphs)"], index=1, key="sel_narr_length")
221
- image_quality_input = st.selectbox("Image Detail/Style:", options=["Standard", "High Detail", "Sketch Concept"], index=0, key="sel_img_quality")
222
 
223
- submit_scene_button = st.form_submit_button("🌌 Weave This Scene!", use_container_width=True, type="primary", disabled=st.session_state.processing_scene)
224
-
225
- if st.button("🎲 Surprise Me!", use_container_width=True, disabled=st.session_state.processing_scene, key="surprise_btn_sidebar"):
226
- sur_prompt, sur_style, sur_artist = surprise_me_func()
227
- st.session_state.form_scene_prompt = sur_prompt
228
- st.session_state.form_image_style = sur_style
229
- st.session_state.form_artist_style = sur_artist
230
- st.experimental_rerun()
231
-
232
- if st.button("πŸ—‘οΈ New Story", use_container_width=True, disabled=st.session_state.processing_scene, key="clear_btn_sidebar"):
233
- st.session_state.story_object = Story()
234
- st.session_state.current_log = ["Story Cleared. Ready for a new verse!"]
235
- st.session_state.latest_scene_image_pil = None
236
- st.session_state.latest_scene_narrative = "## ✨ A New Story Begins ✨\nDescribe your first scene!"
237
- st.session_state.status_message = {"text": "πŸ“œ Story Cleared. A fresh canvas awaits!", "type": "processing"}
238
- st.session_state.form_scene_prompt = "" # Clear form input too
239
- st.experimental_rerun()
 
 
 
 
240
 
241
  with st.expander("πŸ”§ AI Services Status", expanded=False):
242
- text_llm_ok, image_gen_ok = (AI_SERVICES_STATUS["gemini_text_ready"] or AI_SERVICES_STATUS["hf_text_ready"]), \
243
- (AI_SERVICES_STATUS["dalle_image_ready"] or AI_SERVICES_STATUS["hf_image_ready"])
244
  if not text_llm_ok and not image_gen_ok: st.error("CRITICAL: NO AI SERVICES CONFIGURED.")
245
  else:
246
- if text_llm_ok: st.success("Text Generation Service(s) Ready.")
247
- else: st.warning("Text Generation Service(s) NOT Ready.")
248
- if image_gen_ok: st.success("Image Generation Service(s) Ready.")
249
- else: st.warning("Image Generation Service(s) NOT Ready.")
250
 
251
 
252
  # --- Main Display Area ---
253
- col_main_left, col_main_right = st.columns([2,3]) # Define columns for latest scene and gallery
 
 
 
 
 
254
 
255
- with col_main_left:
256
- st.subheader("🌠 Latest Scene")
257
- if st.session_state.processing_scene and st.session_state.latest_scene_image_pil is None :
 
 
258
  st.image(create_placeholder_image_st("🎨 Conjuring visuals..."), use_column_width="always")
259
  elif st.session_state.latest_scene_image_pil:
260
  st.image(st.session_state.latest_scene_image_pil, use_column_width="always", caption="Latest Generated Image")
261
  else:
262
  st.image(create_placeholder_image_st("Describe a scene to begin!", size=(512,300), color="#1A1A2E"), use_column_width="always")
263
-
264
- if st.session_state.processing_scene and "Musing narrative..." in st.session_state.latest_scene_narrative:
265
- st.markdown(" Musing narrative...")
266
- else:
267
- st.markdown(st.session_state.latest_scene_narrative, unsafe_allow_html=True)
268
 
269
- with col_main_right:
270
- st.subheader("πŸ“š Story Scroll")
271
  if st.session_state.story_object and st.session_state.story_object.scenes:
272
- num_columns_gallery = 2
273
  gallery_cols = st.columns(num_columns_gallery)
274
  scenes_for_gallery = st.session_state.story_object.get_all_scenes_for_gallery_display()
275
  for i, (img, caption) in enumerate(scenes_for_gallery):
276
  with gallery_cols[i % num_columns_gallery]:
277
- if img: st.image(img, caption=caption if caption else f"Scene {i+1}", use_column_width="always", output_format="PNG") # Specify output format for PIL
278
- elif caption: st.caption(caption)
279
- else: st.caption(f"Scene {i+1} (No image)")
280
  else:
281
- st.caption("Your story scroll is empty. Weave your first scene in the panel on the left!")
282
 
283
- # Status Bar (displayed below main content)
284
- st.markdown("---")
285
- status_type = st.session_state.status_message.get("type", "processing")
286
- st.markdown(f"<p class='status-message status-{status_type}'>{st.session_state.status_message['text']}</p>", unsafe_allow_html=True)
287
-
288
-
289
- # Interaction Log
290
- with st.expander("βš™οΈ Interaction Log (Newest First)", expanded=False):
291
- log_display = "\n\n---\n\n".join(st.session_state.current_log[::-1][:30])
292
  st.markdown(log_display, unsafe_allow_html=True)
293
 
294
  # --- Logic for Form Submission ---
295
- if submit_scene_button:
296
- if not scene_prompt_text_input.strip(): # Use the _input variable name
297
  st.session_state.status_message = {"text": "Scene prompt cannot be empty!", "type": "error"}
298
- # No explicit rerun here, warning will show, user can submit again.
299
- # Or: st.warning("Scene prompt cannot be empty!"); st.stop()
300
  else:
301
  st.session_state.processing_scene = True
302
  st.session_state.status_message = {"text": f"🌌 Weaving Scene {st.session_state.story_object.current_scene_number + 1}...", "type": "processing"}
303
  st.session_state.current_log.append(f"**πŸš€ Scene {st.session_state.story_object.current_scene_number + 1} - {time.strftime('%H:%M:%S')}**")
304
- # Values from widgets are automatically available by their keys
305
- # e.g., scene_prompt_text_input, image_style_dropdown_input (from key="sel_image_style")
306
- # However, for clarity, we re-fetch them here from their input widget keys.
 
 
 
 
 
 
 
 
307
 
308
- _scene_prompt = scene_prompt_text_input # From form
309
- _image_style = image_style_dropdown_input # From form
310
- _artist_style = artist_style_text_input # From form
311
- _negative_prompt = negative_prompt_text_input # From form
312
- _text_model = text_model_key_input # From form
313
- _image_provider = image_provider_key_input # From form
314
- _narr_length = narrative_length_input # From form
315
- _img_quality = image_quality_input # From form
316
-
317
  # ---- Main Generation Logic ----
318
  current_narrative_text = f"Narrative Error: Init failed."
319
  generated_image_pil = None
320
  image_gen_error_msg = None
321
- final_scene_error_msg = None
322
 
323
  # 1. Generate Narrative
324
- text_model_info = TEXT_MODELS.get(_text_model)
325
- if text_model_info and text_model_info["type"] != "none":
326
- system_p = get_narrative_system_prompt("default"); prev_narr = st.session_state.story_object.get_last_scene_narrative(); user_p = format_narrative_user_prompt(_scene_prompt, prev_narr)
327
- st.session_state.current_log.append(f" Narrative: Using {_text_model} ({text_model_info['id']}).")
328
- text_resp = None
329
- if text_model_info["type"] == "gemini" and AI_SERVICES_STATUS["gemini_text_ready"]: text_resp = generate_text_gemini(user_p, model_id=text_model_info["id"], system_prompt=system_p, max_tokens=768 if _narr_length.startswith("Detailed") else 400)
330
- elif text_model_info["type"] == "hf_text" and AI_SERVICES_STATUS["hf_text_ready"]: text_resp = generate_text_hf(user_p, model_id=text_model_info["id"], system_prompt=system_p, max_tokens=768 if _narr_length.startswith("Detailed") else 400)
331
- if text_resp and text_resp.success: current_narrative_text = basic_text_cleanup(text_resp.text); st.session_state.current_log.append(" Narrative: Success.")
332
- elif text_resp: current_narrative_text = f"**Narrative Error:** {text_resp.error}"; st.session_state.current_log.append(f" Narrative: FAILED - {text_resp.error}")
333
- else: st.session_state.current_log.append(f" Narrative: FAILED - No response.")
334
- else: current_narrative_text = "**Narrative Error:** Model unavailable."; st.session_state.current_log.append(f" Narrative: FAILED - Model '{_text_model}' unavailable.")
 
335
  st.session_state.latest_scene_narrative = f"## Scene Idea: {_scene_prompt}\n\n{current_narrative_text}"
336
 
337
  # 2. Generate Image
338
- selected_img_prov_type = IMAGE_PROVIDERS.get(_image_provider)
339
- img_content_prompt = current_narrative_text if current_narrative_text and "Error" not in current_narrative_text else _scene_prompt
340
- quality_kw = "ultra detailed, " if _img_quality == "High Detail" else ("concept sketch, " if _img_quality == "Sketch Concept" else "")
341
- full_img_prompt_for_gen = format_image_generation_prompt(quality_kw + img_content_prompt[:350], _image_style, _artist_style)
342
- st.session_state.current_log.append(f" Image: Using {_image_provider} (type '{selected_img_prov_type}').")
343
- if selected_img_prov_type and selected_img_prov_type != "none":
344
- img_resp = None
345
- if selected_img_prov_type.startswith("dalle_") and AI_SERVICES_STATUS["dalle_image_ready"]:
346
- dalle_model = "dall-e-3" if selected_img_prov_type == "dalle_3" else "dall-e-2"
347
- img_resp = generate_image_dalle(full_img_prompt_for_gen, model=dalle_model)
348
- elif selected_img_prov_type.startswith("hf_") and AI_SERVICES_STATUS["hf_image_ready"]:
349
- hf_model_id = "stabilityai/stable-diffusion-xl-base-1.0" # Default
350
- if selected_img_prov_type == "hf_openjourney": hf_model_id="prompthero/openjourney"
351
- img_resp = generate_image_hf_model(full_img_prompt_for_gen, model_id=hf_model_id, negative_prompt=_negative_prompt)
352
- if img_resp and img_resp.success: generated_image_pil = img_resp.image; st.session_state.current_log.append(" Image: Success.")
353
- elif img_resp: image_gen_error_msg = f"**Image Error:** {img_resp.error}"; st.session_state.current_log.append(f" Image: FAILED - {img_resp.error}")
354
- else: image_gen_error_msg = "**Image Error:** No response."; st.session_state.current_log.append(" Image: FAILED - No response.")
355
- else: image_gen_error_msg = "**Image Error:** No provider."; st.session_state.current_log.append(f" Image: FAILED - No provider.")
 
356
  st.session_state.latest_scene_image_pil = generated_image_pil if generated_image_pil else create_placeholder_image_st("Image Gen Failed", color="#401010")
357
 
358
  # 3. Add Scene
359
- if image_gen_error_msg and "**Narrative Error**" in current_narrative_text: final_scene_error_msg = f"{current_narrative_text}\n{image_gen_error_msg}" # etc.
360
  elif "**Narrative Error**" in current_narrative_text: final_scene_error_msg = current_narrative_text
361
  elif image_gen_error_msg: final_scene_error_msg = image_gen_error_msg
 
362
  st.session_state.story_object.add_scene_from_elements(
363
  user_prompt=_scene_prompt, narrative_text=current_narrative_text, image=generated_image_pil,
364
  image_style_prompt=f"{_image_style}{f', by {_artist_style}' if _artist_style else ''}",
@@ -367,8 +311,10 @@ if submit_scene_button:
367
  st.session_state.current_log.append(f" Scene {st.session_state.story_object.current_scene_number} processed.")
368
 
369
  # 4. Set final status message
370
- if final_scene_error_msg: st.session_state.status_message = {"text": f"Scene {st.session_state.story_object.current_scene_number} added with errors.", "type": "error"}
371
- else: st.session_state.status_message = {"text": f"🌌 Scene {st.session_state.story_object.current_scene_number} woven!", "type": "success"}
 
 
372
 
373
  st.session_state.processing_scene = False
374
- st.experimental_rerun()
 
1
+ # app.py (Streamlit version for StoryVerse Weaver - FINAL FULL VERSION)
2
  import streamlit as st # FIRST STREAMLIT IMPORT
3
+ from PIL import Image, ImageDraw, ImageFont
4
  import os
5
  import time
6
  import random
7
+ import traceback
8
 
9
  # --- Page Configuration (MUST BE THE VERY FIRST STREAMLIT COMMAND) ---
10
  st.set_page_config(
 
15
  )
16
  # --- END OF PAGE CONFIG ---
17
 
 
 
 
 
 
 
 
 
 
18
  # --- Core Logic Imports (NOW AFTER st.set_page_config) ---
19
  from core.llm_services import initialize_text_llms, is_gemini_text_ready, is_hf_text_ready, generate_text_gemini, generate_text_hf, LLMTextResponse
20
  from core.image_services import initialize_image_llms, is_dalle_ready, is_hf_image_api_ready, generate_image_dalle, generate_image_hf_model, ImageGenResponse
 
79
  <style>
80
  body { color: #D0D0E0; background-color: #0F0F1A; font-family: 'Lexend Deca', sans-serif;}
81
  .stApp { background-color: #0F0F1A; }
82
+ h1 { font-size: 2.8em !important; text-align: center; color: transparent !important; background: linear-gradient(135deg, #A020F0 0%, #E040FB 100%); -webkit-background-clip: text; background-clip: text; margin-bottom: 5px !important; letter-spacing: -1px;}
83
+ h3 { color: #C080F0 !important; text-align: center; font-weight: 400; margin-bottom: 25px !important;}
84
+ .main .block-container { padding-top: 2rem; padding-bottom: 2rem; padding-left: 2rem; padding-right: 2rem; max-width: 1400px; margin: auto; background-color: #1A1A2E; border-radius: 15px; box-shadow: 0 8px 24px rgba(0,0,0,0.15);}
85
+ [data-testid="stSidebar"] { background-color: #131325; border-right: 1px solid #2A2A4A; padding: 1rem;}
86
+ [data-testid="stSidebar"] .stMarkdown h3 { color: #D0D0FF !important; font-size: 1.5em; border-bottom: 2px solid #7F00FF; padding-bottom: 5px;}
87
+ .stTextInput > div > div > input, .stTextArea > div > div > textarea, .stSelectbox > div > div > div[data-baseweb="select"] > div, div[data-baseweb="input"] > input { background-color: #2A2A4A !important; color: #E0E0FF !important; border: 1px solid #4A4A6A !important; border-radius: 8px !important;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  .stButton > button {
89
  background: linear-gradient(135deg, #7F00FF 0%, #E100FF 100%) !important;
90
  color: white !important; border: none !important; border-radius: 8px !important;
91
  padding: 0.7em 1.3em !important; font-weight: 600 !important;
92
  box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important;
93
+ transition: all 0.2s ease-in-out; width: 100%;
 
94
  }
95
  .stButton > button:hover { transform: scale(1.03) translateY(-1px); box-shadow: 0 8px 16px rgba(127,0,255,0.3) !important; }
96
  .stButton > button:disabled { background: #4A4A6A !important; color: #8080A0 !important; cursor: not-allowed; }
97
+ button[kind="secondary"] { background: #4A4A6A !important; color: #E0E0FF !important;} /* For other buttons if not primary */
98
+ button[kind="secondary"]:hover { background: #5A5A7A !important; }
 
 
 
 
 
 
99
  .stImage > img { border-radius: 12px; box-shadow: 0 6px 15px rgba(0,0,0,0.25); max-height: 550px; margin: auto; display: block;}
 
100
  .stExpander { background-color: #161628; border: 1px solid #2A2A4A; border-radius: 12px; margin-bottom: 1em;}
101
  .stExpander header { font-size: 1.1em; font-weight: 500; color: #D0D0FF;}
 
102
  .important-note { background-color: rgba(127,0,255,0.1); border-left: 5px solid #7F00FF; padding: 15px; margin-bottom:20px; color: #E0E0FF; border-radius: 6px;}
 
103
  .status-message { padding: 10px; border-radius: 6px; margin-top: 10px; text-align: center; font-weight: 500; }
104
  .status-success { background-color: #104010; color: #B0FFB0; border: 1px solid #208020; }
105
  .status-error { background-color: #401010; color: #FFB0B0; border: 1px solid #802020; }
106
  .status-processing { background-color: #102040; color: #B0D0FF; border: 1px solid #204080; }
 
107
  .gallery-col img { border-radius: 8px; box-shadow: 0 2px 6px rgba(0,0,0,0.15); margin-bottom: 5px;}
108
+ .gallery-col .stCaption { font-size: 0.85em; text-align: center; color: #A0A0C0;}
109
  </style>
110
  """
111
  st.markdown(streamlit_omega_css, unsafe_allow_html=True)
 
123
  draw.text(((size[0]-tw)/2, (size[1]-th)/2), text, font=font, fill=text_color); return img
124
 
125
  # --- Initialize Session State ---
126
+ if 'story_object' not in st.session_state: st.session_state.story_object = Story()
127
+ if 'current_log' not in st.session_state: st.session_state.current_log = ["Welcome to StoryVerse Weaver!"]
128
+ if 'latest_scene_image_pil' not in st.session_state: st.session_state.latest_scene_image_pil = None
129
+ if 'latest_scene_narrative' not in st.session_state: st.session_state.latest_scene_narrative = "## ✨ A New Story Begins ✨\nDescribe your first scene idea in the panel on the left!"
130
+ if 'status_message' not in st.session_state: st.session_state.status_message = {"text": "Ready to weave your first masterpiece!", "type": "processing"}
131
+ if 'processing_scene' not in st.session_state: st.session_state.processing_scene = False
 
 
 
 
 
 
132
  if 'form_scene_prompt' not in st.session_state: st.session_state.form_scene_prompt = ""
133
  if 'form_image_style' not in st.session_state: st.session_state.form_image_style = "Default (Cinematic Realism)"
134
  if 'form_artist_style' not in st.session_state: st.session_state.form_artist_style = ""
135
 
136
+ # --- UI Definition ---
137
+ st.markdown("<div align='center'><h1>✨ StoryVerse Weaver ✨</h1>\n<h3>Craft Immersive Multimodal Worlds with AI</h3></div>", unsafe_allow_html=True)
138
+ st.markdown("<div class='important-note'><strong>Welcome, Worldsmith!</strong> Describe your vision, choose your style, and let Weaver help you craft captivating scenes with narrative and imagery. Ensure API keys (<code>STORYVERSE_...</code>) are correctly set in your environment/secrets!</div>", unsafe_allow_html=True)
 
 
139
 
140
  # --- Sidebar for Inputs & Configuration ---
141
  with st.sidebar:
142
  st.markdown("### πŸ’‘ **Craft Your Scene**")
143
+ with st.form("scene_input_form_key", clear_on_submit=False): # Keep values after submit for reference
144
+ scene_prompt_text_val = st.text_area("Scene Vision (Description, Dialogue, Action):",
145
+ value=st.session_state.form_scene_prompt, height=150,
146
+ placeholder="e.g., A lone astronaut discovers a glowing alien artifact...")
 
 
 
 
147
  st.markdown("#### 🎨 Visual Style")
148
  col_style1, col_style2 = st.columns(2)
149
  with col_style1:
150
+ image_style_val = st.selectbox("Style Preset:",
151
+ options=["Default (Cinematic Realism)"] + sorted(list(STYLE_PRESETS.keys())),
152
+ index=(["Default (Cinematic Realism)"] + sorted(list(STYLE_PRESETS.keys()))).index(st.session_state.form_image_style) if st.session_state.form_image_style in (["Default (Cinematic Realism)"] + sorted(list(STYLE_PRESETS.keys()))) else 0)
153
  with col_style2:
154
+ artist_style_val = st.text_input("Artistic Inspiration (Optional):",
155
+ value=st.session_state.form_artist_style, placeholder="e.g., Moebius")
156
 
157
+ negative_prompt_val = st.text_area("Exclude from Image (Negative Prompt):", value=COMMON_NEGATIVE_PROMPTS, height=80)
158
 
159
  with st.expander("βš™οΈ Advanced AI Configuration", expanded=False):
160
+ text_model_val = st.selectbox("Narrative AI Engine:", options=list(TEXT_MODELS.keys()),
161
+ index=list(TEXT_MODELS.keys()).index(UI_DEFAULT_TEXT_MODEL_KEY) if UI_DEFAULT_TEXT_MODEL_KEY in TEXT_MODELS else 0)
162
+ image_provider_val = st.selectbox("Visual AI Engine:", options=list(IMAGE_PROVIDERS.keys()),
163
+ index=list(IMAGE_PROVIDERS.keys()).index(UI_DEFAULT_IMAGE_PROVIDER_KEY) if UI_DEFAULT_IMAGE_PROVIDER_KEY in IMAGE_PROVIDERS else 0)
164
+ narrative_length_val = st.selectbox("Narrative Detail:", ["Short (1 paragraph)", "Medium (2-3 paragraphs)", "Detailed (4+ paragraphs)"], index=1)
165
+ image_quality_val = st.selectbox("Image Detail:", ["Standard", "High Detail", "Sketch Concept"], index=0)
 
 
166
 
167
+ submit_button_val = st.form_submit_button("🌌 Weave Scene!", use_container_width=True, type="primary", disabled=st.session_state.processing_scene)
168
+
169
+ # Buttons outside the form
170
+ col_btn1, col_btn2 = st.columns(2)
171
+ with col_btn1:
172
+ if st.button("🎲 Surprise Me!", use_container_width=True, disabled=st.session_state.processing_scene, key="sidebar_surprise_btn"):
173
+ sur_prompt, sur_style, sur_artist = surprise_me_func()
174
+ st.session_state.form_scene_prompt = sur_prompt
175
+ # To make selectbox update, its key should also be updated or use a callback if Streamlit version supports
176
+ st.session_state.form_image_style = sur_style
177
+ st.session_state.form_artist_style = sur_artist
178
+ st.rerun()
179
+ with col_btn2:
180
+ if st.button("πŸ—‘οΈ New Story", use_container_width=True, disabled=st.session_state.processing_scene, key="sidebar_clear_btn", type="secondary"): # type="secondary" is conceptual
181
+ st.session_state.story_object = Story()
182
+ st.session_state.current_log = ["Story Cleared."]
183
+ st.session_state.latest_scene_image_pil = None
184
+ st.session_state.latest_scene_narrative = "## ✨ New Story Begins ✨"
185
+ st.session_state.status_message = {"text": "πŸ“œ Story Cleared.", "type": "processing"}
186
+ st.session_state.form_scene_prompt = ""
187
+ st.rerun()
188
 
189
  with st.expander("πŸ”§ AI Services Status", expanded=False):
190
+ # ... (API status HTML generation as before, using AI_SERVICES_STATUS) ...
191
+ text_llm_ok, image_gen_ok = (AI_SERVICES_STATUS["gemini_text_ready"] or AI_SERVICES_STATUS["hf_text_ready"]), (AI_SERVICES_STATUS["dalle_image_ready"] or AI_SERVICES_STATUS["hf_image_ready"])
192
  if not text_llm_ok and not image_gen_ok: st.error("CRITICAL: NO AI SERVICES CONFIGURED.")
193
  else:
194
+ if text_llm_ok: st.success("Text Generation Ready.")
195
+ else: st.warning("Text Generation NOT Ready.")
196
+ if image_gen_ok: st.success("Image Generation Ready.")
197
+ else: st.warning("Image Generation NOT Ready.")
198
 
199
 
200
  # --- Main Display Area ---
201
+ st.markdown("---") # Horizontal line
202
+ st.markdown("### πŸ–ΌοΈ **Your Evolving StoryVerse**", unsafe_allow_html=True)
203
+
204
+ # Status Bar
205
+ status_type = st.session_state.status_message.get("type", "processing")
206
+ st.markdown(f"<p class='status-message status-{status_type}'>{st.session_state.status_message['text']}</p>", unsafe_allow_html=True)
207
 
208
+ # Tabs for display
209
+ tab_latest, tab_scroll, tab_log = st.tabs(["🌠 Latest Scene", "πŸ“š Story Scroll", "βš™οΈ Interaction Log"])
210
+
211
+ with tab_latest:
212
+ if st.session_state.processing_scene and st.session_state.latest_scene_image_pil is None:
213
  st.image(create_placeholder_image_st("🎨 Conjuring visuals..."), use_column_width="always")
214
  elif st.session_state.latest_scene_image_pil:
215
  st.image(st.session_state.latest_scene_image_pil, use_column_width="always", caption="Latest Generated Image")
216
  else:
217
  st.image(create_placeholder_image_st("Describe a scene to begin!", size=(512,300), color="#1A1A2E"), use_column_width="always")
218
+ st.markdown(st.session_state.latest_scene_narrative, unsafe_allow_html=True)
 
 
 
 
219
 
220
+ with tab_scroll:
 
221
  if st.session_state.story_object and st.session_state.story_object.scenes:
222
+ num_columns_gallery = st.slider("Gallery Columns:", 1, 5, 3, key="gallery_cols_slider")
223
  gallery_cols = st.columns(num_columns_gallery)
224
  scenes_for_gallery = st.session_state.story_object.get_all_scenes_for_gallery_display()
225
  for i, (img, caption) in enumerate(scenes_for_gallery):
226
  with gallery_cols[i % num_columns_gallery]:
227
+ if img: st.image(img, caption=caption if caption else f"Scene {i+1}", use_column_width="always", output_format="PNG")
228
+ elif caption: st.caption(f"Scene {i+1} (No Image)\n{caption}")
229
+ else: st.caption(f"Scene {i+1} (No image or caption)")
230
  else:
231
+ st.caption("Your story scroll is empty. Weave your first scene!")
232
 
233
+ with tab_log:
234
+ log_display = "\n\n---\n\n".join(st.session_state.current_log[::-1][:50])
 
 
 
 
 
 
 
235
  st.markdown(log_display, unsafe_allow_html=True)
236
 
237
  # --- Logic for Form Submission ---
238
+ if submit_button_val:
239
+ if not scene_prompt_text_val.strip():
240
  st.session_state.status_message = {"text": "Scene prompt cannot be empty!", "type": "error"}
241
+ st.rerun()
 
242
  else:
243
  st.session_state.processing_scene = True
244
  st.session_state.status_message = {"text": f"🌌 Weaving Scene {st.session_state.story_object.current_scene_number + 1}...", "type": "processing"}
245
  st.session_state.current_log.append(f"**πŸš€ Scene {st.session_state.story_object.current_scene_number + 1} - {time.strftime('%H:%M:%S')}**")
246
+ st.rerun() # Rerun to show "processing" and disable button (will re-execute from top)
247
+
248
+ # Store form values to use after the rerun, as widget values reset
249
+ _scene_prompt = scene_prompt_text_val
250
+ _image_style = image_style_val
251
+ _artist_style = artist_style_val
252
+ _negative_prompt = negative_prompt_val
253
+ _text_model = text_model_val
254
+ _image_provider = image_provider_val
255
+ _narr_length = narrative_length_val
256
+ _img_quality = image_quality_val
257
 
 
 
 
 
 
 
 
 
 
258
  # ---- Main Generation Logic ----
259
  current_narrative_text = f"Narrative Error: Init failed."
260
  generated_image_pil = None
261
  image_gen_error_msg = None
262
+ final_scene_error_msg = None # For consolidating errors
263
 
264
  # 1. Generate Narrative
265
+ with st.spinner("✍️ Crafting narrative... (This may take a moment)"):
266
+ text_model_info = TEXT_MODELS.get(_text_model)
267
+ if text_model_info and text_model_info["type"] != "none":
268
+ system_p = get_narrative_system_prompt("default"); prev_narr = st.session_state.story_object.get_last_scene_narrative(); user_p = format_narrative_user_prompt(_scene_prompt, prev_narr)
269
+ st.session_state.current_log.append(f" Narrative: Using {_text_model} ({text_model_info['id']}). Length: {_narr_length}")
270
+ text_resp = None
271
+ if text_model_info["type"] == "gemini" and AI_SERVICES_STATUS["gemini_text_ready"]: text_resp = generate_text_gemini(user_p, model_id=text_model_info["id"], system_prompt=system_p, max_tokens=768 if _narr_length.startswith("Detailed") else 400)
272
+ elif text_model_info["type"] == "hf_text" and AI_SERVICES_STATUS["hf_text_ready"]: text_resp = generate_text_hf(user_p, model_id=text_model_info["id"], system_prompt=system_p, max_tokens=768 if _narr_length.startswith("Detailed") else 400)
273
+ if text_resp and text_resp.success: current_narrative_text = basic_text_cleanup(text_resp.text); st.session_state.current_log.append(" Narrative: Success.")
274
+ elif text_resp: current_narrative_text = f"**Narrative Error ({_text_model}):** {text_resp.error}"; st.session_state.current_log.append(f" Narrative: FAILED - {text_resp.error}")
275
+ else: st.session_state.current_log.append(f" Narrative: FAILED - No response.")
276
+ else: current_narrative_text = "**Narrative Error:** Model unavailable."; st.session_state.current_log.append(f" Narrative: FAILED - Model '{_text_model}' unavailable.")
277
  st.session_state.latest_scene_narrative = f"## Scene Idea: {_scene_prompt}\n\n{current_narrative_text}"
278
 
279
  # 2. Generate Image
280
+ with st.spinner("🎨 Conjuring visuals... (This may take a moment)"):
281
+ selected_img_prov_type = IMAGE_PROVIDERS.get(_image_provider)
282
+ img_content_prompt = current_narrative_text if current_narrative_text and "Error" not in current_narrative_text else _scene_prompt
283
+ quality_kw = "ultra detailed, " if _img_quality == "High Detail" else ("concept sketch, " if _img_quality == "Sketch Concept" else "")
284
+ full_img_prompt_for_gen = format_image_generation_prompt(quality_kw + img_content_prompt[:350], _image_style, _artist_style)
285
+ st.session_state.current_log.append(f" Image: Using {_image_provider} (type '{selected_img_prov_type}').")
286
+ if selected_img_prov_type and selected_img_prov_type != "none":
287
+ img_resp = None
288
+ if selected_img_prov_type.startswith("dalle_") and AI_SERVICES_STATUS["dalle_image_ready"]:
289
+ dalle_model = "dall-e-3" if selected_img_prov_type == "dalle_3" else "dall-e-2"
290
+ img_resp = generate_image_dalle(full_img_prompt_for_gen, model=dalle_model)
291
+ elif selected_img_prov_type.startswith("hf_") and AI_SERVICES_STATUS["hf_image_ready"]:
292
+ hf_model_id = "stabilityai/stable-diffusion-xl-base-1.0" # Default
293
+ if selected_img_prov_type == "hf_openjourney": hf_model_id="prompthero/openjourney"
294
+ img_resp = generate_image_hf_model(full_img_prompt_for_gen, model_id=hf_model_id, negative_prompt=_negative_prompt)
295
+ if img_resp and img_resp.success: generated_image_pil = img_resp.image; st.session_state.current_log.append(" Image: Success.")
296
+ elif img_resp: image_gen_error_msg = f"**Image Error:** {img_resp.error}"; st.session_state.current_log.append(f" Image: FAILED - {img_resp.error}")
297
+ else: image_gen_error_msg = "**Image Error:** No response."; st.session_state.current_log.append(" Image: FAILED - No response.")
298
+ else: image_gen_error_msg = "**Image Error:** No provider configured."; st.session_state.current_log.append(f" Image: FAILED - No provider.")
299
  st.session_state.latest_scene_image_pil = generated_image_pil if generated_image_pil else create_placeholder_image_st("Image Gen Failed", color="#401010")
300
 
301
  # 3. Add Scene
302
+ if image_gen_error_msg and "**Narrative Error**" in current_narrative_text: final_scene_error_msg = f"Narrative: {current_narrative_text.split('**')[-1].strip()} \nImage: {image_gen_error_msg.split('**')[-1].strip()}"
303
  elif "**Narrative Error**" in current_narrative_text: final_scene_error_msg = current_narrative_text
304
  elif image_gen_error_msg: final_scene_error_msg = image_gen_error_msg
305
+
306
  st.session_state.story_object.add_scene_from_elements(
307
  user_prompt=_scene_prompt, narrative_text=current_narrative_text, image=generated_image_pil,
308
  image_style_prompt=f"{_image_style}{f', by {_artist_style}' if _artist_style else ''}",
 
311
  st.session_state.current_log.append(f" Scene {st.session_state.story_object.current_scene_number} processed.")
312
 
313
  # 4. Set final status message
314
+ if final_scene_error_msg:
315
+ st.session_state.status_message = {"text": f"Scene {st.session_state.story_object.current_scene_number} added with errors.", "type": "error"}
316
+ else:
317
+ st.session_state.status_message = {"text": f"🌌 Scene {st.session_state.story_object.current_scene_number} woven successfully!", "type": "success"}
318
 
319
  st.session_state.processing_scene = False
320
+ st.rerun() # Final rerun to update UI with results and re-enable button