Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| from PIL import Image | |
| import time | |
| from huggingface_hub import InferenceClient | |
| import imageio | |
| import numpy as np | |
| # Using 'black-forest-labs/FLUX.1-schnell' for fast, reliable image generation. | |
| # Can be overridden via environment variables if needed. | |
| T2I_SPACE = os.environ.get("T2I_SPACE", "black-forest-labs/FLUX.1-schnell") | |
| def generate_exercise_video(exercise_name, output_format): | |
| if not exercise_name or len(exercise_name.strip()) == 0: | |
| return None, None, gr.Image(), gr.Video() | |
| # Construct an optimized storyboard prompt | |
| prompt = ( | |
| f"A 3-panel horizontal storyboard showing a fit person performing the exercise: {exercise_name}. " | |
| "Panel 1: starting position, Panel 2: midpoint action, Panel 3: starting position. " | |
| "Three side-by-side horizontal panels, clean white background, high quality, consistent character." | |
| ) | |
| try: | |
| hf_token = os.environ.get("HF_TOKEN") | |
| print(f"Connecting to serverless Inference Client for {T2I_SPACE}...") | |
| client = InferenceClient(T2I_SPACE, token=hf_token) | |
| print("Generating storyboard image via serverless API...") | |
| img = client.text_to_image(prompt, width=1024, height=512) | |
| w, h = img.size | |
| print(f"Storyboard generated with size: {w}x{h}. Processing frames...") | |
| # Split into 3 horizontal frames | |
| frame_width = w // 3 | |
| frames = [] | |
| for i in range(3): | |
| box = (i * frame_width, 0, (i + 1) * frame_width, h) | |
| frame = img.crop(box) | |
| frames.append(frame) | |
| # Compile into a looping frame sequence (Frame 1 -> Frame 2 -> Frame 3 -> Frame 2) | |
| loop_frames = [frames[0], frames[1], frames[2], frames[1]] | |
| # Ensure temporary directory exists | |
| tmp_dir = os.path.join(os.getcwd(), "tmp_animations") | |
| os.makedirs(tmp_dir, exist_ok=True) | |
| timestamp = int(time.time()) | |
| # 1. Save as Animated GIF | |
| gif_path = os.path.join(tmp_dir, f"exercise_animation_{timestamp}.gif") | |
| loop_frames[0].save( | |
| gif_path, | |
| save_all=True, | |
| append_images=loop_frames[1:], | |
| duration=500, # 500ms per frame | |
| loop=0 | |
| ) | |
| print(f"Successfully saved looping GIF to: {gif_path}") | |
| # 2. Save as MP4 Video | |
| mp4_path = os.path.join(tmp_dir, f"exercise_animation_{timestamp}.mp4") | |
| # Resize to even dimensions for standard video codec compatibility | |
| w_even = (frame_width // 2) * 2 | |
| h_even = (h // 2) * 2 | |
| processed_video_frames = [] | |
| for f in loop_frames: | |
| if f.size != (w_even, h_even): | |
| f = f.resize((w_even, h_even), Image.Resampling.LANCZOS) | |
| processed_video_frames.append(np.array(f)) | |
| # Write MP4 (fps=2 matches 500ms duration per frame) | |
| # macro_block_size=None keeps the exact resized dimensions (e.g. 340x512) | |
| writer = imageio.get_writer(mp4_path, fps=2, macro_block_size=None) | |
| for frame in processed_video_frames: | |
| writer.append_data(frame) | |
| writer.close() | |
| print(f"Successfully saved looping MP4 to: {mp4_path}") | |
| # Return files and visibility configurations depending on chosen format | |
| if output_format == "Animated GIF": | |
| return ( | |
| gif_path, | |
| mp4_path, | |
| gr.Image(visible=True, value=gif_path), | |
| gr.Video(visible=False, value=None) | |
| ) | |
| else: | |
| return ( | |
| gif_path, | |
| mp4_path, | |
| gr.Image(visible=False, value=None), | |
| gr.Video(visible=True, value=mp4_path, autoplay=True, loop=True) | |
| ) | |
| except Exception as e: | |
| print(f"Error generating animation: {e}") | |
| raise gr.Error(f"Failed to animate exercise. The API returned an error: {e}") | |
| # Build the Gradio Interface | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# 🏋️ Exercise Animator") | |
| gr.Markdown("Type the name of an exercise to see an AI-generated animation of how to perform it.") | |
| # Store file paths so switching format doesn't require regeneration | |
| gif_state = gr.State(value=None) | |
| mp4_state = gr.State(value=None) | |
| with gr.Row(): | |
| with gr.Column(): | |
| exercise_input = gr.Textbox( | |
| label="Exercise Name", | |
| placeholder="e.g., Jumping Jacks, Squats, Pushups...", | |
| lines=1 | |
| ) | |
| format_choice = gr.Radio( | |
| choices=["Animated GIF", "Video (MP4)"], | |
| value="Animated GIF", | |
| label="Output Format" | |
| ) | |
| submit_btn = gr.Button("Animate", variant="primary") | |
| with gr.Column(): | |
| video_output_image = gr.Image(label="Generated Animation (GIF)", type="filepath", visible=True) | |
| video_output_video = gr.Video(label="Generated Animation (Video)", visible=False) | |
| submit_btn.click( | |
| fn=generate_exercise_video, | |
| inputs=[exercise_input, format_choice], | |
| outputs=[gif_state, mp4_state, video_output_image, video_output_video] | |
| ) | |
| def toggle_format(choice, gif_path, mp4_path): | |
| if choice == "Animated GIF": | |
| return ( | |
| gr.Image(visible=True, value=gif_path), | |
| gr.Video(visible=False, value=None) | |
| ) | |
| else: | |
| return ( | |
| gr.Image(visible=False, value=None), | |
| gr.Video(visible=True, value=mp4_path, autoplay=True, loop=True) | |
| ) | |
| format_choice.change( | |
| fn=toggle_format, | |
| inputs=[format_choice, gif_state, mp4_state], | |
| outputs=[video_output_image, video_output_video] | |
| ) | |
| gr.Examples( | |
| examples=["Jumping Jacks", "Squats", "Pushups", "Burpees", "Yoga Downward Dog"], | |
| inputs=[exercise_input] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(theme=gr.themes.Soft(), server_name="0.0.0.0") | |