Spaces:
Sleeping
Sleeping
| import os | |
| import uuid | |
| import tempfile | |
| import gradio as gr | |
| from gtts import gTTS | |
| from PIL import Image | |
| import pytesseract | |
| from moviepy import ImageClip, AudioFileClip, concatenate_videoclips | |
| def set_clip_duration(clip, duration): | |
| if hasattr(clip, "with_duration"): | |
| return clip.with_duration(duration) | |
| return clip.set_duration(duration) | |
| def set_clip_fps(clip, fps): | |
| if hasattr(clip, "with_fps"): | |
| return clip.with_fps(fps) | |
| return clip.set_fps(fps) | |
| def resize_clip_height(clip, height): | |
| if hasattr(clip, "resized"): | |
| return clip.resized(height=height) | |
| return clip.resize(height=height) | |
| def attach_audio(video_clip, audio_clip): | |
| if hasattr(video_clip, "with_audio"): | |
| return video_clip.with_audio(audio_clip) | |
| return video_clip.set_audio(audio_clip) | |
| def extract_text_from_image(image_path): | |
| """ | |
| Reads visible text from a screenshot using OCR. | |
| Works best when screenshots contain dashboard labels, KPI names, chart titles, or table text. | |
| """ | |
| image = Image.open(image_path) | |
| text = pytesseract.image_to_string(image) | |
| cleaned_lines = [] | |
| for line in text.splitlines(): | |
| line = line.strip() | |
| if line: | |
| cleaned_lines.append(line) | |
| cleaned_text = " ".join(cleaned_lines) | |
| return cleaned_text | |
| def create_ai_narration(slide_number, total_slides, extracted_text): | |
| """ | |
| Creates a simple narration script from OCR text. | |
| This is intentionally lightweight for Hugging Face Free CPU. | |
| """ | |
| if extracted_text.strip() == "": | |
| return ( | |
| f"This is slide {slide_number} of {total_slides}. " | |
| "This screen appears to show a visual dashboard or report. " | |
| "The key takeaway should be reviewed based on the visible charts, metrics, and layout." | |
| ) | |
| # Limit text so narration does not become too long | |
| max_chars = 450 | |
| short_text = extracted_text[:max_chars] | |
| if slide_number == 1: | |
| intro = "This first screen introduces the main dashboard view." | |
| elif slide_number == total_slides: | |
| intro = "This final screen summarizes the key information shown in the report." | |
| else: | |
| intro = f"This is slide {slide_number} of {total_slides}." | |
| narration = ( | |
| f"{intro} " | |
| f"The visible information includes: {short_text}. " | |
| "The main point is to review the key metrics, compare the important values, " | |
| "and identify where business attention may be required." | |
| ) | |
| return narration | |
| def create_slide_video(image_path, narration_text, output_height, temp_dir, run_id, slide_number): | |
| """ | |
| Creates one slide video: | |
| screenshot + AI-generated narration audio. | |
| """ | |
| audio_path = os.path.join( | |
| temp_dir, | |
| f"slide_audio_{run_id}_{slide_number}.mp3" | |
| ) | |
| tts = gTTS(text=narration_text, lang="en") | |
| tts.save(audio_path) | |
| audio = AudioFileClip(audio_path) | |
| audio_duration = audio.duration | |
| slide_duration = audio_duration + 0.5 | |
| image_clip = ImageClip(image_path) | |
| image_clip = set_clip_duration(image_clip, slide_duration) | |
| image_clip = resize_clip_height(image_clip, output_height) | |
| image_clip = set_clip_fps(image_clip, 24) | |
| slide_clip = attach_audio(image_clip, audio) | |
| return slide_clip, audio, audio_path, narration_text | |
| def create_video_from_screenshots(images, output_height): | |
| if not images: | |
| return None, None, "Please upload at least one screenshot or image.", "" | |
| try: | |
| output_height = int(output_height) | |
| if output_height < 240: | |
| return None, None, "Output height should be at least 240.", "" | |
| run_id = str(uuid.uuid4()) | |
| temp_dir = tempfile.gettempdir() | |
| final_video_path = os.path.join( | |
| temp_dir, | |
| f"ai_narrated_video_{run_id}.mp4" | |
| ) | |
| total_slides = len(images) | |
| slide_clips = [] | |
| audio_clips = [] | |
| audio_paths = [] | |
| generated_narrations = [] | |
| for index, image_path in enumerate(images): | |
| slide_number = index + 1 | |
| extracted_text = extract_text_from_image(image_path) | |
| narration_text = create_ai_narration( | |
| slide_number=slide_number, | |
| total_slides=total_slides, | |
| extracted_text=extracted_text | |
| ) | |
| slide_clip, audio_clip, audio_path, final_narration = create_slide_video( | |
| image_path=image_path, | |
| narration_text=narration_text, | |
| output_height=output_height, | |
| temp_dir=temp_dir, | |
| run_id=run_id, | |
| slide_number=slide_number | |
| ) | |
| slide_clips.append(slide_clip) | |
| audio_clips.append(audio_clip) | |
| audio_paths.append(audio_path) | |
| generated_narrations.append( | |
| f"Slide {slide_number}:\n{final_narration}" | |
| ) | |
| final_video = concatenate_videoclips( | |
| slide_clips, | |
| method="compose" | |
| ) | |
| final_video.write_videofile( | |
| final_video_path, | |
| codec="libx264", | |
| audio_codec="aac", | |
| fps=24, | |
| preset="ultrafast", | |
| threads=2 | |
| ) | |
| final_duration = final_video.duration | |
| final_video.close() | |
| for clip in slide_clips: | |
| clip.close() | |
| for audio in audio_clips: | |
| audio.close() | |
| narration_preview = "\n\n".join(generated_narrations) | |
| return ( | |
| final_video_path, | |
| audio_paths[0], | |
| f"Success: AI-generated narrated video created. Slides: {total_slides}. Duration: {final_duration:.1f} seconds.", | |
| narration_preview | |
| ) | |
| except Exception as e: | |
| return None, None, f"Error: {str(e)}", "" | |
| with gr.Blocks(title="AI Screenshot to Narrated Video Generator") as demo: | |
| gr.Markdown( | |
| """ | |
| # AI Screenshot to Narrated Video Generator | |
| Upload screenshots and the app will automatically: | |
| 1. Read text from each screenshot | |
| 2. Generate narration for each slide | |
| 3. Convert narration to audio | |
| 4. Create a narrated video | |
| No manual narration text is required. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| images_input = gr.Files( | |
| label="Upload Screenshots / Images", | |
| file_types=["image"], | |
| type="filepath" | |
| ) | |
| height_input = gr.Dropdown( | |
| label="Output Video Height", | |
| choices=[360, 480, 720], | |
| value=480 | |
| ) | |
| generate_button = gr.Button("Generate AI Narrated Video") | |
| with gr.Column(): | |
| video_output = gr.Video(label="Generated Video") | |
| audio_output = gr.Audio(label="First Slide Audio Preview") | |
| status_output = gr.Textbox(label="Status") | |
| narration_output = gr.Textbox( | |
| label="Generated Narration Preview", | |
| lines=12 | |
| ) | |
| generate_button.click( | |
| fn=create_video_from_screenshots, | |
| inputs=[ | |
| images_input, | |
| height_input | |
| ], | |
| outputs=[ | |
| video_output, | |
| audio_output, | |
| status_output, | |
| narration_output | |
| ] | |
| ) | |
| demo.queue() | |
| demo.launch() |