agharsallah commited on
Commit
c323310
Β·
1 Parent(s): 69aaf62

adding Progress bar on chapter creation

Browse files
app.py CHANGED
@@ -8,6 +8,7 @@ from dotenv import load_dotenv
8
  from pathlib import Path
9
  from ui.styles import main_css
10
  from ui.components import (
 
11
  create_header,
12
  create_instructions_accordion,
13
  create_story_tab_header,
@@ -108,18 +109,71 @@ def create_app():
108
  chapters_state = gr.State(value=None)
109
 
110
  # Chapter processing loading animation
111
- create_chapter_loading_container()
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  @gr.render(inputs=[chapters_state])
114
  def render_chapters(chapters_data):
115
  if chapters_data is None:
116
  return create_empty_chapters_placeholder()
117
 
118
- # Check if chapters_data is a string indicating an error
119
  if isinstance(chapters_data, str) and chapters_data.startswith(
120
  "Error:"
121
  ):
122
  return create_chapter_error_display(chapters_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  # Display story title with magical styling
125
  create_story_title_display(
@@ -192,6 +246,7 @@ def create_app():
192
  story_title=story_title,
193
  story_text=story_text,
194
  chapters_state=chapters_state,
 
195
  )
196
 
197
  return demo
 
8
  from pathlib import Path
9
  from ui.styles import main_css
10
  from ui.components import (
11
+ create_chapter_loading_placeholder,
12
  create_header,
13
  create_instructions_accordion,
14
  create_story_tab_header,
 
109
  chapters_state = gr.State(value=None)
110
 
111
  # Chapter processing loading animation
112
+ chapter_loading_container = create_chapter_loading_container()
113
+
114
+ # Function to show the chapter loading container when processing starts
115
+ def show_chapter_progress_container():
116
+ return gr.update(visible=True)
117
+
118
+ # Connect process_chapters_button to show the loading container
119
+ process_chapters_button.click(
120
+ show_chapter_progress_container,
121
+ inputs=[],
122
+ outputs=[chapter_loading_container],
123
+ )
124
 
125
  @gr.render(inputs=[chapters_state])
126
  def render_chapters(chapters_data):
127
  if chapters_data is None:
128
  return create_empty_chapters_placeholder()
129
 
130
+ # Check if chapters_data is a string indicating an error or dict with error key
131
  if isinstance(chapters_data, str) and chapters_data.startswith(
132
  "Error:"
133
  ):
134
  return create_chapter_error_display(chapters_data)
135
+ elif isinstance(chapters_data, dict) and "error" in chapters_data:
136
+ return create_chapter_error_display(chapters_data["error"])
137
+
138
+ # Check if we're still processing
139
+ if isinstance(chapters_data, dict) and chapters_data.get(
140
+ "processing", False
141
+ ):
142
+ # If chapters exist but we're still processing, show them with loading indicators
143
+ chapters = chapters_data.get("chapters", [])
144
+ if chapters:
145
+ # Display story title with magical styling
146
+ create_story_title_display(
147
+ chapters_data.get("title", "Story Chapters")
148
+ )
149
+
150
+ # Chapter navigation
151
+ create_chapter_navigation()
152
+
153
+ # Display each chapter with appropriate loading indicators
154
+ for i, chapter in enumerate(chapters, 1):
155
+ status = chapter.get(
156
+ "status", "Creating your magical illustration..."
157
+ )
158
+
159
+ (
160
+ read_aloud_btn,
161
+ read_aloud_audio,
162
+ chapter_content_state,
163
+ audio_statuss,
164
+ ) = create_chapter_accordion(index=i, chapter=chapter)
165
+
166
+ # Add audio generation functionality
167
+ read_aloud_btn.click(
168
+ generate_audio_with_status,
169
+ inputs=[chapter_content_state],
170
+ outputs=[read_aloud_audio, audio_statuss],
171
+ )
172
+
173
+ return gr.update()
174
+ else:
175
+ # If no chapters yet, show a processing message
176
+ return create_chapter_loading_placeholder()
177
 
178
  # Display story title with magical styling
179
  create_story_title_display(
 
246
  story_title=story_title,
247
  story_text=story_text,
248
  chapters_state=chapters_state,
249
+ chapter_loading_container=chapter_loading_container,
250
  )
251
 
252
  return demo
controllers/app_controller.py CHANGED
@@ -1,6 +1,6 @@
1
  import logging
2
  import json
3
- from typing import Optional, List, Any, Union, Tuple
4
  from services.story_generator import generate_story
5
  from services.pdf_text_extractor import extract_text_from_pdf
6
  from services.streaming_chapter_processor import process_story_into_chapters_streaming
@@ -135,6 +135,20 @@ def process_chapters(
135
  current_data = {"title": story_title, "chapters": chapters}
136
 
137
  # Count completed images
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  except Exception as e:
140
  logger.error(f"Error in update callback: {e}")
@@ -142,12 +156,12 @@ def process_chapters(
142
  return current_data
143
 
144
  # Start the streaming process
 
145
  process_story_into_chapters_streaming(
146
  story_content, story_title, update_callback=update_callback
147
  )
148
 
149
  # Return the final data structure
150
-
151
  return current_data
152
 
153
  except Exception as e:
@@ -156,18 +170,82 @@ def process_chapters(
156
 
157
 
158
  # Add chapter processing functionality
159
- def handle_chapter_processing(story_content, story_title):
160
  """Handle chapter processing and update state"""
161
  if not story_content or story_content.startswith("Error:"):
162
- return "Error: Please generate a valid story first."
163
- gr.update(interactive=False)
164
  gr.Info(
165
  message="Processing story into chapters... <br> Go to the Chapters tab to see updates.",
166
  title="Processing",
167
  )
 
168
  # Process chapters and return the data structure
169
- result = process_chapters(story_content, story_title)
170
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
 
173
  def generate_audio_with_status(text):
 
1
  import logging
2
  import json
3
+ from typing import Optional, List, Any, Union, Tuple, Dict
4
  from services.story_generator import generate_story
5
  from services.pdf_text_extractor import extract_text_from_pdf
6
  from services.streaming_chapter_processor import process_story_into_chapters_streaming
 
135
  current_data = {"title": story_title, "chapters": chapters}
136
 
137
  # Count completed images
138
+ total_chapters = len(chapters)
139
+ completed_images = sum(
140
+ 1 for chapter in chapters if chapter.get("image_b64", "")
141
+ )
142
+
143
+ # Update progress
144
+ if total_chapters > 0:
145
+ # First 50% is chapter creation, second 50% is image generation
146
+ chapter_progress = 0.5 # Chapters are already created at this point
147
+ image_progress = 0.5 * (completed_images / total_chapters)
148
+ progress(
149
+ (chapter_progress + image_progress),
150
+ f"Generated {completed_images}/{total_chapters} chapter images",
151
+ )
152
 
153
  except Exception as e:
154
  logger.error(f"Error in update callback: {e}")
 
156
  return current_data
157
 
158
  # Start the streaming process
159
+ progress(0.05, "Splitting story into chapters...")
160
  process_story_into_chapters_streaming(
161
  story_content, story_title, update_callback=update_callback
162
  )
163
 
164
  # Return the final data structure
 
165
  return current_data
166
 
167
  except Exception as e:
 
170
 
171
 
172
  # Add chapter processing functionality
173
+ def handle_chapter_processing(story_content, story_title, progress=gr.Progress()):
174
  """Handle chapter processing and update state"""
175
  if not story_content or story_content.startswith("Error:"):
176
+ return {"error": "Please generate a valid story first."}
 
177
  gr.Info(
178
  message="Processing story into chapters... <br> Go to the Chapters tab to see updates.",
179
  title="Processing",
180
  )
181
+
182
  # Process chapters and return the data structure
183
+ logger.info("Starting chapter processing...")
184
+ progress(0.01, "Starting chapter processing...")
185
+
186
+ try:
187
+ # Store for the current chapters data
188
+ current_data = {"title": story_title, "chapters": [], "processing": True}
189
+
190
+ # Callback function to update the UI with each new chapter image
191
+ def update_callback(chapters_json):
192
+ nonlocal current_data
193
+ try:
194
+ chapters_data = json.loads(chapters_json)
195
+
196
+ # Handle progress updates
197
+ if "progress" in chapters_data:
198
+ prog_data = chapters_data["progress"]
199
+ prog_value = prog_data.get("completed", 0) / prog_data.get(
200
+ "total", 1
201
+ )
202
+ prog_message = prog_data.get("message", "Processing chapters...")
203
+ progress(prog_value, prog_message)
204
+
205
+ # Handle error cases
206
+ if "error" in chapters_data:
207
+ current_data = {
208
+ "title": story_title,
209
+ "error": chapters_data["error"],
210
+ }
211
+ return current_data
212
+
213
+ # Update chapters if present
214
+ if "chapters" in chapters_data:
215
+ chapters = chapters_data.get("chapters", [])
216
+ current_data = {
217
+ "title": story_title,
218
+ "chapters": chapters,
219
+ "processing": True,
220
+ }
221
+
222
+ # Check if processing is complete
223
+ if (
224
+ "progress" in chapters_data
225
+ and chapters_data["progress"].get("stage") == "complete"
226
+ ):
227
+ current_data["processing"] = False
228
+
229
+ except Exception as e:
230
+ logger.error(f"Error in update callback: {e}")
231
+ current_data = {
232
+ "title": story_title,
233
+ "error": f"Error in update: {str(e)}",
234
+ }
235
+
236
+ return current_data
237
+
238
+ # Start the streaming process
239
+ process_story_into_chapters_streaming(
240
+ story_content, story_title, update_callback=update_callback
241
+ )
242
+
243
+ # Return the final data structure
244
+ return current_data
245
+
246
+ except Exception as e:
247
+ logger.error(f"Failed to process chapters: {e}", exc_info=True)
248
+ return {"title": story_title, "error": f"Error processing chapters: {str(e)}"}
249
 
250
 
251
  def generate_audio_with_status(text):
services/streaming_chapter_processor.py CHANGED
@@ -87,6 +87,18 @@ class StreamingChapterProcessor:
87
  str: JSON string with updated chapters as images are generated
88
  """
89
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  # Create prompt for splitting the text
91
  prompt = f"""Split the following story into logical chapters (3-5 chapters). And do not include any additional text or explanations. Include all the chapter content.
92
  For each chapter:
@@ -111,6 +123,18 @@ Here's the story to split:
111
  {story_text}
112
  """
113
  try:
 
 
 
 
 
 
 
 
 
 
 
 
114
  result = self.mistral_api.send_request(prompt)
115
  logger.info("Getting chapter splitting request from Mistral API")
116
 
@@ -118,13 +142,36 @@ Here's the story to split:
118
  content = result["choices"][0]["message"]["content"]
119
  chapters_data = self._extract_chapters_from_api_response(content)
120
 
121
- # First yield the chapters without images
122
- # Add empty image_b64 fields to chapters
 
 
 
 
 
 
 
 
 
 
 
123
  for chapter in chapters_data:
124
  chapter["image_b64"] = ""
 
125
 
126
  # First yield the chapters without images
127
- yield json.dumps({"chapters": chapters_data}, indent=2)
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  # Start image generation in parallel
130
  with concurrent.futures.ThreadPoolExecutor() as executor:
@@ -136,6 +183,7 @@ Here's the story to split:
136
  # Keep track of which chapters have been updated
137
  updated_chapters = list(chapters_data)
138
  chapters_remaining = len(chapters_data)
 
139
 
140
  # Yield updates as images are generated
141
  while chapters_remaining > 0:
@@ -143,13 +191,45 @@ Here's the story to split:
143
  # Wait for up to 1 second for a new image
144
  index, updated_chapter = self.chapter_queue.get(timeout=1)
145
  updated_chapters[index] = updated_chapter
 
146
  chapters_remaining -= 1
 
147
 
148
- # Yield the updated chapters list
149
- yield json.dumps({"chapters": updated_chapters}, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  except queue.Empty:
151
  # No new images yet, continue waiting
152
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  else:
154
  logger.error("Invalid response format from Mistral API")
155
  yield json.dumps({"error": "Invalid response format from Mistral API"})
 
87
  str: JSON string with updated chapters as images are generated
88
  """
89
 
90
+ # First yield a progress update indicating we're starting
91
+ yield json.dumps(
92
+ {
93
+ "progress": {
94
+ "stage": "starting",
95
+ "message": "Starting chapter processing...",
96
+ "completed": 0,
97
+ "total": 1,
98
+ }
99
+ }
100
+ )
101
+
102
  # Create prompt for splitting the text
103
  prompt = f"""Split the following story into logical chapters (3-5 chapters). And do not include any additional text or explanations. Include all the chapter content.
104
  For each chapter:
 
123
  {story_text}
124
  """
125
  try:
126
+ # Update progress - sending API request
127
+ yield json.dumps(
128
+ {
129
+ "progress": {
130
+ "stage": "analyzing",
131
+ "message": "Analyzing story and creating chapters...",
132
+ "completed": 0.1,
133
+ "total": 1,
134
+ }
135
+ }
136
+ )
137
+
138
  result = self.mistral_api.send_request(prompt)
139
  logger.info("Getting chapter splitting request from Mistral API")
140
 
 
142
  content = result["choices"][0]["message"]["content"]
143
  chapters_data = self._extract_chapters_from_api_response(content)
144
 
145
+ # Update progress - chapters created
146
+ yield json.dumps(
147
+ {
148
+ "progress": {
149
+ "stage": "chapters_created",
150
+ "message": "Chapters created! Preparing for image generation...",
151
+ "completed": 0.3,
152
+ "total": 1,
153
+ }
154
+ }
155
+ )
156
+
157
+ # Add empty image_b64 fields and progress status to chapters
158
  for chapter in chapters_data:
159
  chapter["image_b64"] = ""
160
+ chapter["status"] = "Waiting for image generation..."
161
 
162
  # First yield the chapters without images
163
+ yield json.dumps(
164
+ {
165
+ "chapters": chapters_data,
166
+ "progress": {
167
+ "stage": "chapters_created",
168
+ "message": "Chapters created! Starting image generation...",
169
+ "completed": 0.4,
170
+ "total": 1,
171
+ },
172
+ },
173
+ indent=2,
174
+ )
175
 
176
  # Start image generation in parallel
177
  with concurrent.futures.ThreadPoolExecutor() as executor:
 
183
  # Keep track of which chapters have been updated
184
  updated_chapters = list(chapters_data)
185
  chapters_remaining = len(chapters_data)
186
+ completed_images = 0
187
 
188
  # Yield updates as images are generated
189
  while chapters_remaining > 0:
 
191
  # Wait for up to 1 second for a new image
192
  index, updated_chapter = self.chapter_queue.get(timeout=1)
193
  updated_chapters[index] = updated_chapter
194
+ updated_chapters[index]["status"] = "Image generated!"
195
  chapters_remaining -= 1
196
+ completed_images += 1
197
 
198
+ # Calculate progress percentage (40% for chapters creation + 60% for image generation)
199
+ progress_value = 0.4 + (
200
+ 0.6 * completed_images / len(chapters_data)
201
+ )
202
+
203
+ # Yield the updated chapters list with progress information
204
+ yield json.dumps(
205
+ {
206
+ "chapters": updated_chapters,
207
+ "progress": {
208
+ "stage": "generating_images",
209
+ "message": f"Generating chapter images: {completed_images}/{len(chapters_data)} complete",
210
+ "completed": progress_value,
211
+ "total": 1,
212
+ },
213
+ },
214
+ indent=2,
215
+ )
216
  except queue.Empty:
217
  # No new images yet, continue waiting
218
  continue
219
+
220
+ # Final progress update
221
+ yield json.dumps(
222
+ {
223
+ "chapters": updated_chapters,
224
+ "progress": {
225
+ "stage": "complete",
226
+ "message": "All chapters and images are ready!",
227
+ "completed": 1.0,
228
+ "total": 1,
229
+ },
230
+ },
231
+ indent=2,
232
+ )
233
  else:
234
  logger.error("Invalid response format from Mistral API")
235
  yield json.dumps({"error": "Invalid response format from Mistral API"})
ui/components.py CHANGED
@@ -250,10 +250,10 @@ def create_story_output_container():
250
 
251
  def create_chapter_loading_container():
252
  """Create the loading animation container for chapters"""
253
- with gr.Row(visible=False) as container:
254
  gr.HTML("""
255
- <div class="loading-animation">
256
- 🎨 <span style="color: #ff9057;">Creating illustrated chapters...</span> 🎨
257
  </div>
258
  """)
259
  return container
@@ -262,16 +262,36 @@ def create_chapter_loading_container():
262
  def create_empty_chapters_placeholder():
263
  """Create the placeholder for when no chapters are available"""
264
  return gr.HTML("""
265
- <div class="image-header-wrapper">
266
  <img src="/gradio_api/file=assets/images/empty_bk.png" style="width: 150px; margin-bottom: 20px;" alt="Empty Book">
267
  <h1 class="sub-header" style="margin-left: 16px; padding-bottom: 36px;">
268
  Your Illustrated Story Awaits!
269
  </h1>
270
  </div>
271
- <p class="text" style="font-size: 1.2em; text-align: center;">First, create your story and then click the "Create Illustrated Chapters!" button to see your story come to life with pictures!</p>
272
  """)
273
 
274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  def create_chapter_error_display(error_message):
276
  """Create an error display for chapter generation issues"""
277
  return gr.HTML(f"""
@@ -344,7 +364,7 @@ def create_chapter_accordion(index, chapter):
344
  gr.HTML("""
345
  <div style="text-align: center; padding: 30px; background-color: #f0f7ff; border-radius: 15px;">
346
  <div class="loading-animation" style="font-size: 30px;">πŸ–ŒοΈ</div>
347
- <p style="color: #5d9df5; font-weight: bold; margin-top: 15px;">Creating your magical illustration...</p>
348
  </div>
349
  """)
350
 
 
250
 
251
  def create_chapter_loading_container():
252
  """Create the loading animation container for chapters"""
253
+ with gr.Row(visible=False, elem_id="chapter-loading-container") as container:
254
  gr.HTML("""
255
+ <div id="progress-info"">
256
+ <span id="progress-stage">...</span>
257
  </div>
258
  """)
259
  return container
 
262
  def create_empty_chapters_placeholder():
263
  """Create the placeholder for when no chapters are available"""
264
  return gr.HTML("""
265
+ <div class="image-header-wrapper empty-placeholder" id="empty-chapters-placeholder">
266
  <img src="/gradio_api/file=assets/images/empty_bk.png" style="width: 150px; margin-bottom: 20px;" alt="Empty Book">
267
  <h1 class="sub-header" style="margin-left: 16px; padding-bottom: 36px;">
268
  Your Illustrated Story Awaits!
269
  </h1>
270
  </div>
271
+ <p class="text empty-placeholder" style="font-size: 1.2em; text-align: center;">First, create your story and then click the "Create Illustrated Chapters!" button to see your story come to life with pictures!</p>
272
  """)
273
 
274
 
275
+ def create_chapter_loading_placeholder():
276
+ return gr.HTML("""
277
+ <div style="text-align: center; padding: 30px; background-color: #f0f7ff; border-radius: 15px;">
278
+ <div class="loading-animation" style="font-size: 36px;">βœ¨πŸ“šβœ¨</div>
279
+ <h3 style="color: #5d9df5; margin-top: 20px;">Creating Your Magical Story Chapters...</h3>
280
+ <p style="color: #666; margin-top: 10px;">Our wizard is crafting beautiful chapters and illustrations for your story...</p>
281
+ <div class="small-loader" style="text-align: center; padding: 10px;">
282
+ <div class="spinner" style="display: inline-block; width: 20px; height: 20px;
283
+ border: 3px solid rgba(0, 0, 0, 0.1); border-radius: 50%;
284
+ border-top-color: #3498db; animation: spin 1s linear infinite;"></div>
285
+ <style>
286
+ @keyframes spin {
287
+ to { transform: rotate(360deg); }
288
+ }
289
+ </style>
290
+ </div>
291
+ </div>
292
+ """)
293
+
294
+
295
  def create_chapter_error_display(error_message):
296
  """Create an error display for chapter generation issues"""
297
  return gr.HTML(f"""
 
364
  gr.HTML("""
365
  <div style="text-align: center; padding: 30px; background-color: #f0f7ff; border-radius: 15px;">
366
  <div class="loading-animation" style="font-size: 30px;">πŸ–ŒοΈ</div>
367
+ <p style="color: #5d9df5; font-weight: bold; margin-top: 15px;">{status}</p>
368
  </div>
369
  """)
370
 
ui/events.py CHANGED
@@ -2,6 +2,7 @@
2
  Event handlers for the Magic Story Creator
3
  """
4
 
 
5
  from controllers.app_controller import (
6
  process_story_generation,
7
  clear_fields,
@@ -25,6 +26,7 @@ def setup_event_handlers(
25
  story_title,
26
  story_text,
27
  chapters_state,
 
28
  ):
29
  """Set up all the event handlers for the application"""
30
 
@@ -52,11 +54,33 @@ def setup_event_handlers(
52
  outputs=[subject, story_title, story_text, process_chapters_button],
53
  )
54
 
55
- # Chapter processing event handler
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  process_chapters_button.click(
57
  handle_chapter_processing,
58
  inputs=[story_text, story_title],
59
  outputs=[chapters_state],
 
60
  )
61
 
62
  return generate_button, clear_button, process_chapters_button
 
2
  Event handlers for the Magic Story Creator
3
  """
4
 
5
+ import gradio as gr
6
  from controllers.app_controller import (
7
  process_story_generation,
8
  clear_fields,
 
26
  story_title,
27
  story_text,
28
  chapters_state,
29
+ chapter_loading_container=None,
30
  ):
31
  """Set up all the event handlers for the application"""
32
 
 
54
  outputs=[subject, story_title, story_text, process_chapters_button],
55
  )
56
 
57
+ # Function to show loading container and hide placeholder when processing starts
58
+ def start_chapter_processing(story_text, story_title):
59
+ # Return initial data structure with processing flag
60
+ return {"title": story_title, "processing": True, "chapters": []}
61
+
62
+ # If we have a loading container reference, show it when processing starts
63
+ if chapter_loading_container:
64
+ process_chapters_button.click(
65
+ lambda: gr.update(visible=True),
66
+ inputs=[],
67
+ outputs=[chapter_loading_container],
68
+ )
69
+
70
+ # Show the loading indicator before processing starts and hide placeholder
71
+ process_chapters_button.click(
72
+ start_chapter_processing,
73
+ inputs=[story_text, story_title],
74
+ outputs=[chapters_state],
75
+ js="function() { document.querySelectorAll('.empty-placeholder').forEach(el => el.style.display = 'none');window.scrollTo({ top: 0, behavior: 'smooth' }); return []; }",
76
+ )
77
+
78
+ # Chapter processing event handler - happens after loading container is shown
79
  process_chapters_button.click(
80
  handle_chapter_processing,
81
  inputs=[story_text, story_title],
82
  outputs=[chapters_state],
83
+ show_progress=True, # Show progress bar
84
  )
85
 
86
  return generate_button, clear_button, process_chapters_button