| | import gradio as gr |
| | import time |
| | from utils.story_management import ( |
| | generate_direct_comic, |
| | extract_comic_scenes, |
| | load_narration_from_file |
| | ) |
| | from config import IMAGE_STYLES, IMAGE_STYLE_INFO, AGE_GROUPS |
| | from datetime import datetime |
| |
|
| | def log_execution(func): |
| | def wrapper(*args, **kwargs): |
| | start_time = time.time() |
| | start_str = datetime.fromtimestamp(start_time).strftime('%Y-%m-%d %H:%M:%S') |
| | |
| | result = func(*args, **kwargs) |
| | |
| | end_time = time.time() |
| | end_str = datetime.fromtimestamp(end_time).strftime('%Y-%m-%d %H:%M:%S') |
| | duration = end_time - start_time |
| |
|
| |
|
| | |
| | return result |
| | return wrapper |
| | @log_execution |
| | def create_story_interface(demo: gr.Blocks) -> gr.Blocks: |
| | """Create the main story interface with comic generation functionality. |
| | |
| | This function initializes the primary UI interface for the comic generation system, |
| | setting up the main tab structure and components. |
| | |
| | Args: |
| | demo (gr.Blocks): The Gradio Blocks instance to build the interface on |
| | |
| | Returns: |
| | gr.Blocks: The configured Gradio interface with all components initialized |
| | """ |
| |
|
| | create_quick_comic_tab() |
| |
|
| | return demo |
| | def create_quick_comic_tab() -> None: |
| | """Create a simple tab for direct prompt-to-image comic generation. |
| | |
| | Sets up the main comic generation interface with the following components: |
| | - Story prompt input field |
| | - AI prompt enhancement option |
| | - Visual style selection |
| | - Number of scenes selector |
| | - Generation controls |
| | - Image display area |
| | - Scene navigation system |
| | |
| | The interface allows users to: |
| | 1. Input their story description |
| | 2. Configure generation parameters |
| | 3. Generate a multi-panel comic |
| | 4. View and navigate through individual scenes |
| | """ |
| |
|
| | with gr.Column(): |
| | |
| | gr.Markdown("Welcome to Hekaya ") |
| |
|
| | with gr.Row(): |
| | with gr.Column(scale=3): |
| | user_prompt = gr.Textbox( |
| | label="What Hekaya story would you like to visualize?", |
| | placeholder="Describe your story with main characters and settings... (e.g., 'A young wizard learning magic in an ancient castle')", |
| | lines=4 |
| | ) |
| |
|
| | enrich_prompt = gr.Checkbox( |
| | label="Enhance prompt with AI for coherence", |
| | value=True, |
| | info="Use AI to add just enough detail and coherence for consistent visual storytelling across all scenes" |
| | ) |
| |
|
| | with gr.Column(scale=1): |
| | comic_style = gr.Dropdown( |
| | label="Visual Style", |
| | choices=IMAGE_STYLES, |
| | value="Comic Book Style" |
| | ) |
| |
|
| | style_description = gr.Markdown( |
| | value=f"*{IMAGE_STYLE_INFO['Comic Book Style']}*", |
| | label="Style Description" |
| | ) |
| |
|
| | age_group = gr.Dropdown( |
| | label="Target Age Group", |
| | choices=AGE_GROUPS, |
| | value="9-12 (Pre-teen)", |
| | info="Select the audience age group. Narration language, detail, and length will adapt automatically." |
| | ) |
| |
|
| | image_quality = gr.Dropdown( |
| | label="Image Quality", |
| | choices=["Low", "Medium", "High"], |
| | value="Low", |
| | info="Select the quality level for generated images. Higher quality may take longer to generate." |
| | ) |
| |
|
| | generate_btn = gr.Button("Generate Hekaya Story", variant="primary") |
| | status_display = gr.Markdown("") |
| |
|
| | with gr.Row(): |
| | with gr.Column(scale=2): |
| | comic_image = gr.Image(label="Generated Hekaya Story", type="filepath") |
| |
|
| | with gr.Column(scale=1, elem_id="save_info_container"): |
| | |
| | gr.Markdown("Your generated story images are automatically saved locally.") |
| | save_path_display = gr.Markdown("", elem_id="save_path_info") |
| |
|
| | narration_display = gr.Markdown( |
| | |
| | visible=True, |
| | elem_id="story_narration", |
| | elem_classes=["story-narration-box"] |
| | ) |
| |
|
| | with gr.Column(visible=False) as scene_viewer_container: |
| | |
| | gr.Markdown("Use the navigation buttons to view each upscaled scene individually.") |
| |
|
| | with gr.Row(equal_height=True): |
| | prev_scene_btn = gr.Button("β Previous Scene", variant="secondary") |
| | scene_counter = gr.Markdown("Scene 1 of 1", elem_id="scene_counter") |
| | next_scene_btn = gr.Button("Next Scene β", variant="secondary") |
| |
|
| | scene_image = gr.Image(label="Current Scene", type="filepath", height=768) |
| | scene_caption_display = gr.Markdown("", elem_id="scene_caption", elem_classes=["scene-caption-box"]) |
| | scene_save_path = gr.Markdown("", elem_id="scene_save_path_info") |
| |
|
| | scene_info = gr.State([]) |
| | current_scene_index = gr.State(0) |
| |
|
| |
|
| | def update_style_description(style: str) -> str: |
| | """Update the style description text when a new style is selected.""" |
| | return f"*{IMAGE_STYLE_INFO[style]}*" |
| |
|
| | def show_generating_message() -> str: |
| | """Display a loading message while story scenes are being generated.""" |
| | return "π Generating your story scenes... Please wait..." |
| |
|
| | def generate_comic_with_length(user_prompt, comic_style, enrich_prompt, age_group, image_quality): |
| | """Wrapper that handles the fixed num_scenes value while passing the age group and image quality.""" |
| | comic_image, save_path_display, status_display, narration = generate_direct_comic( |
| | user_prompt, |
| | comic_style, |
| | 12, |
| | enrich_prompt, |
| | 3, |
| | age_group, |
| | ) |
| |
|
| | if narration and narration.strip(): |
| | narration_formatted = f"" |
| | narration_update = gr.update(visible=True, value=narration_formatted) |
| | else: |
| | narration_update = gr.update(visible=True, value="") |
| |
|
| | return comic_image, save_path_display, status_display, narration_update |
| |
|
| | def init_scene_viewer(comic_path: str | None) -> tuple: |
| | """Initialize the scene viewer with extracted scenes from the comic image.""" |
| | if not comic_path: |
| | return [], 0, gr.update(visible=False), None, "", "Scene 0 of 0", "No story image generated" |
| |
|
| | scene_data, save_message = extract_comic_scenes(comic_path, 0) |
| |
|
| | if not scene_data: |
| | return [], 0, gr.update(visible=False), None, "", "Scene 0 of 0", "Failed to extract scenes" |
| |
|
| | first_scene = scene_data[0] |
| |
|
| | return ( |
| | scene_data, |
| | 0, |
| | gr.update(visible=True), |
| | first_scene['path'], |
| | first_scene['caption'], |
| | f"Scene 1 of {len(scene_data)}", |
| | save_message |
| | ) |
| |
|
| | def update_scene_display(scene_data: list, current_index: int) -> tuple: |
| | """Update the scene viewer display with the current scene.""" |
| | if not scene_data: |
| | return None, "", "Scene 0 of 0" |
| |
|
| | index = max(0, min(current_index, len(scene_data) - 1)) |
| | scene = scene_data[index] |
| |
|
| | return scene['path'], scene['caption'], f"Scene {index + 1} of {len(scene_data)}" |
| |
|
| | def navigate_to_previous_scene(idx: int) -> int: |
| | """Navigate to the previous scene in the sequence.""" |
| | return max(0, idx - 1) |
| |
|
| | def navigate_to_next_scene(paths: list, idx: int) -> int: |
| | """Navigate to the next scene in the sequence.""" |
| | return min(len(paths) - 1, idx + 1) if paths else 0 |
| |
|
| |
|
| | comic_style.change( |
| | fn=update_style_description, |
| | inputs=[comic_style], |
| | outputs=[style_description] |
| | ) |
| |
|
| | generate_btn.click( |
| | fn=show_generating_message, |
| | inputs=None, |
| | outputs=status_display |
| | ).then( |
| | fn=generate_comic_with_length, |
| | inputs=[user_prompt, comic_style, enrich_prompt, age_group, image_quality], |
| | outputs=[comic_image, save_path_display, status_display, narration_display] |
| | ).then( |
| | fn=init_scene_viewer, |
| | inputs=[comic_image], |
| | outputs=[ |
| | scene_info, |
| | current_scene_index, |
| | scene_viewer_container, |
| | scene_image, |
| | scene_caption_display, |
| | scene_counter, |
| | scene_save_path |
| | ] |
| | ) |
| |
|
| | prev_scene_btn.click( |
| | fn=navigate_to_previous_scene, |
| | inputs=[current_scene_index], |
| | outputs=[current_scene_index] |
| | ).then( |
| | fn=update_scene_display, |
| | inputs=[scene_info, current_scene_index], |
| | outputs=[scene_image, scene_caption_display, scene_counter] |
| | ) |
| |
|
| | next_scene_btn.click( |
| | fn=navigate_to_next_scene, |
| | inputs=[scene_info, current_scene_index], |
| | outputs=[current_scene_index] |
| | ).then( |
| | fn=update_scene_display, |
| | inputs=[scene_info, current_scene_index], |
| | outputs=[scene_image, scene_caption_display, scene_counter] |
| | ) |