import os from moviepy import ImageClip, concatenate_videoclips, AudioFileClip,TextClip,CompositeVideoClip,vfx import pysrt import json from PIL import Image, ImageDraw, ImageFont def check_file_exists(file_path): """Check if a file exists at the specified path.""" if os.path.isfile(file_path): return True else: raise FileNotFoundError(f"File not found: {file_path}") def check_folder_exists(folder_path): '''Checks if a folder path is valid. ''' if os.path.isdir(folder_path): return True else: raise FileNotFoundError(f"Folder not found at {folder_path}") def get_files(folder, extensions): """ Retrieves files with specified extensions from a folder. Parameters: folder (str): Path to the folder. extensions (tuple): File extensions to include (e.g., ('.jpg', '.png')). Returns: list: List of file paths. """ if os.path.isdir(folder): return [ os.path.join(folder, file) # Files are numbered , so that after sorting they are compiled into the video in that order. for file in sorted(os.listdir(folder),key=lambda x: int(x.split('_')[1].split('.')[0])) if file.lower().endswith(extensions) ] else: raise OSError(f"{folder} not found.") def create_srt(text :str, audio_file : AudioFileClip, outfile_name:str, duration:int, chunk_size=5): # with open(text_file, "r") as file: # words = file.read().split() words = text.split() chars = " ".join(words) chars_count = len(chars) word_count = len(words) # word_duration = audio_file.duration / word_count #seconds per word char_duration = audio_file.duration / chars_count #seconds per character # Generate subtitle file subs = pysrt.SubRipFile() start_time = duration # Automatic chunk_size calculation # target_duration = 2 # Number of seconds the subtitle is displayed on the screen # chunk_size = round(target_duration/word_duration) for i in range(0, word_count, chunk_size): chunk = " ".join(words[i:i + chunk_size]) end_time = start_time + (len(chunk) * char_duration) subtitle = pysrt.SubRipItem(index=len(subs) + 1, start=pysrt.SubRipTime(seconds=start_time), end=pysrt.SubRipTime(seconds=end_time), text=chunk) subs.append(subtitle) start_time = end_time out = f"samples/subtitles/.srt/{outfile_name}.srt" subs.save(out) return out def extract_topic_from_json(file_path): ''' extract_topic_from_json extract() takes json file path as input. - Opens the file as read-only and loads the JSON data from it. - Extracts the topic from the JSON data. On success, it returns the topic of the video. ''' try: # Open the JSON file with open(file_path, 'r') as file: # Load JSON data from the file data = json.load(file) # Extract the topic, and audio_script from the JSON data topic = data.get('topic', 'No topic found') return topic except FileNotFoundError: print(f"Error: The file {file_path} was not found.") except json.JSONDecodeError: print(f"Error: The file {file_path} contains invalid JSON.") except Exception as e: print("heloo2") print(f"An unexpected error occurred: {e}") def extract_audio_from_json(file_path): ''' extract_audio_topic_from_json() takes json file path as input. - Opens the file as read-only and loads the JSON data from it. - Extracts the audio_script from the JSON data. On success, it returns audio_script. ''' try: # Open the JSON file with open(file_path, 'r') as file: # Load JSON data from the file data = json.load(file) print(data) # Extract the topic, audio_script and visual_script topic = data.get('topic', 'No topic found') audio_script = data.get('audio_script', []) # visual_script = data.get('visual_script', []) return audio_script except FileNotFoundError: print(f"Error: The file {file_path} was not found.") except json.JSONDecodeError: print(f"Error: The file {file_path} contains invalid JSON.") except Exception as e: print(file_path) print(f"An unexpected error occurred: {e}") def json_extract(json_path): ''' json_extract() takes json file path as input. - Calls the extract_audio_from_json() to extract the text-to-speech / subtitles from the json file, and the topic of the video. On success, it returns the subtitles in list format, and the topic. ''' # Extract parameters from json file audio_script = extract_audio_from_json(json_path) if audio_script: # print("Extracted Audio Parameters:") audio_data = [] for item in audio_script: if 'text' in item: text = item['text'] audio_data.append(text) return audio_data else: raise FileNotFoundError("No audio script found in the JSON file.") def add_effects(clip): """ Adds a effect from a curated list to the video clip. Parameters: clip (VideoClip): Video clip to which effects are to be added. Returns: VideoClip: Video clip with one effect applied. """ random_effect =[vfx.FadeIn(duration=1),vfx.FadeOut(duration=1)] # print(random_effect) return clip.with_effects(random_effect) def create_intro_clip(background_image_path, duration, topic, font_path): """ Create an intro video clip with a background image and centered text. Parameters: background_image_path (str): Path to the background image. duration (int or float): Duration of the clip in seconds. topic (str): The text to display. Defaults to "Welcome to My Video!". font_path (str): Path to the TrueType font file. font_size (int): Size of the text font. text_color (str): Color of the text. Returns: VideoClip: A composite video clip with the background and centered text. """ check_file_exists(background_image_path) # Create an ImageClip for the background image background = ImageClip(background_image_path, duration=duration) # Create a TextClip for the intro text text_clip = TextClip(text=topic, size=(900, 90), method='caption', color="white", font=font_path) # Position the text in the center and set its duration to match the background text_clip = text_clip.with_position("center").with_duration(duration) # Overlay the text clip on top of the background image final_clip = CompositeVideoClip([background, text_clip]) return final_clip def create_video(image_folder: str, audio_folder: str, script_path: str, font_path: str, output_file: str, with_subtitles: bool = False): """ Main function that creates the video. The function works in 3 parts: 1. Checks if the given parameters are correct. 2. If `with_subtitle` flag is set to `False`, creates a video with the images and audio in the given folders. Each image is displayed with the same duration as the corresponding audio file. 3. If the `with_subtitle` flag is set to `True`, embeds subtitles within the video itself, cannot be turned off in video players. Video is compiled using the `compose` method so that if the images are of different aspect ratios /resolutions then the video takes the image with the largest resolution or aspect ratio as the default one and puts black bars with the rest of the non-fitting images. """ check_folder_exists(audio_folder) check_file_exists(script_path) check_file_exists(font_path) # Create a placeholder image if no images are provided if not image_folder or not os.path.exists(image_folder) or not os.listdir(image_folder): placeholder_image = create_placeholder_image(font_path=font_path, text="No Image Available") images = [placeholder_image] * len(get_files(audio_folder, ('.mp3', '.wav'))) else: check_folder_exists(image_folder) images = get_files(image_folder, ('.jpg', '.png')) audio_files = get_files(audio_folder, ('.mp3', '.wav')) subtitles = json_extract(script_path) raw_clips = [] audio_durations = [] Start_duration = 0 # Creating the intro clip and appending it to raw clips path_to_background = "resources/intro/intro.jpg" check_file_exists(path_to_background) topic = extract_topic_from_json(script_path) intro_clip = create_intro_clip(path_to_background, duration=5, topic=topic, font_path=font_path) raw_clips.append(intro_clip) # Create different clips with audio for img, audio in zip(images, audio_files): audio_clip = AudioFileClip(audio) image_clip = ImageClip(img).with_duration(audio_clip.duration).with_audio(audio_clip) audio_durations.append(audio_clip.duration) print(f"Video Clip no. {images.index(img) + 1} successfully created") Start_duration += audio_clip.duration image_clip = add_effects(image_clip) raw_clips.append(image_clip) # Creating the outro clip and appending it to raw clips outro_text = "made with Love by Team EDU-FLICK" outro_clip = create_intro_clip(path_to_background, duration=5, topic=outro_text, font_path=font_path) raw_clips.append(outro_clip) video = concatenate_videoclips(raw_clips, method="compose") if with_subtitles: Start_duration = 5 subtitle_clips = [] chunk_size = 10 for text, duration in zip(subtitles, audio_durations): words = text.split() if len(words) > chunk_size: for i in range(0, len(words), chunk_size): chunk = " ".join(words[i:i + chunk_size]) chunk_duration = duration * (len(chunk.split()) / len(words)) subtitle_clip = TextClip( text=chunk, font=font_path, color='white', bg_color='black', size=(1000, 100), method='caption', text_align="center", horizontal_align="center" ).with_duration(chunk_duration).with_start(Start_duration).with_position('bottom') subtitle_clips.append(subtitle_clip) Start_duration += chunk_duration else: subtitle_clip = TextClip( text=text, font=font_path, color='white', bg_color='black', size=(1000, 100), method='caption', text_align="center", horizontal_align="center" ).with_duration(duration).with_start(Start_duration).with_position('bottom') subtitle_clips.append(subtitle_clip) Start_duration += duration subtitle_clips.insert(0, video) final_video = CompositeVideoClip(subtitle_clips) else: final_video = video final_video.write_videofile(output_file, fps=24, threads=os.cpu_count()) print(f"Video created successfully: {output_file}") def create_placeholder_image(width=1920, height=1080, text="No Image", font_path=None, font_size=50, text_color="white", bg_color="black"): """ Creates a placeholder image with the specified dimensions and text. Parameters: width (int): Width of the image. height (int): Height of the image. text (str): Text to display on the image. font_path (str): Path to the font file. font_size (int): Size of the font. text_color (str): Color of the text. bg_color (str): Background color of the image. Returns: str: Path to the generated placeholder image. """ img = Image.new("RGB", (width, height), bg_color) draw = ImageDraw.Draw(img) if font_path: font = ImageFont.truetype(font_path, font_size) else: font = ImageFont.load_default() # Use textbbox to calculate the bounding box of the text bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] # Calculate width from bounding box text_height = bbox[3] - bbox[1] # Calculate height from bounding box # Center the text text_position = ((width - text_width) // 2, (height - text_height) // 2) # Draw the text on the image draw.text(text_position, text, font=font, fill=text_color) # Save the placeholder image placeholder_path = "placeholder.png" img.save(placeholder_path) return placeholder_path def create_complete_srt(script_folder :str, audio_file_folder : str, outfile_path:str, chunk_size=10): """ Creates an SRT file by extracting subtitles from the script_folder using `json_extract` function and audio files from the `audio_file` folder. Segments the subtitles into the specified chunk size and maps the duration of the chunk to the proportion of the length of the chunk. Parameters: script_folder (str): Path to the folder containing script json file. audio_file_folder (str): Path to the folder containing audio files. outfile_path (str): Path or Name of the SRT file given in output. chunk_size (str): Number of words per subtitle chunk. """ script_folder=r"resources/scripts/script.json"; script = json_extract(script_folder) print(script) audio_files = get_files(audio_file_folder,(".wav",".mp3")) print(audio_files) audio_clips = [] [audio_clips.append(AudioFileClip(x)) for x in audio_files] subs = pysrt.SubRipFile() start_time = 5 chunk = '' chunk_duration = 0 end_time = 5 n = 1 for text,audio_clip in zip(script,audio_clips): duration = audio_clip.duration words = text.split() if len(words) > chunk_size: for i in range(0,len(words),chunk_size): chunk = " ".join(words[i : (i+chunk_size if i < len(words)-1 else len(words)-1)]) chunk_duration = duration * (len(chunk.split())/len(words)) end_time += chunk_duration subtitle = pysrt.SubRipItem( index=n, start=pysrt.SubRipTime(seconds=start_time), end=pysrt.SubRipTime(seconds=end_time), text=chunk ) subs.append(subtitle) # For Debugging: # print(f"Subtitle no. {n} added successfully.") # print(f"Start : {start_time}") # print(f"End : {end_time}") start_time = end_time n+=1 else: chunk = text chunk_duration = duration end_time += chunk_duration subtitle = pysrt.SubRipItem( index=len(subs) + 1, start=pysrt.SubRipTime(seconds=start_time), end=pysrt.SubRipTime(seconds=end_time), text=chunk ) subs.append(subtitle) # For Debugging: print(f"Subtitle no. {n} added successfully.") # print(f"Start : {start_time}") # print(f"End : {end_time}") start_time = end_time n+=1 subs.save(outfile_path) print(f"File saved successfully at {outfile_path}") import os from moviepy import ImageClip, concatenate_videoclips, AudioFileClip, TextClip, CompositeVideoClip, vfx import pysrt import json from PIL import Image, ImageDraw, ImageFont def check_file_exists(file_path): """Check if a file exists at the specified path.""" if os.path.isfile(file_path): return True else: raise FileNotFoundError(f"File not found: {file_path}") def check_folder_exists(folder_path): '''Checks if a folder path is valid. ''' if os.path.isdir(folder_path): return True else: raise FileNotFoundError(f"Folder not found at {folder_path}") def get_files(folder, extensions): """ Retrieves files with specified extensions from a folder. """ if os.path.isdir(folder): files = [] for file in os.listdir(folder): if file.lower().endswith(extensions): files.append(os.path.join(folder, file)) # Sort files numerically based on segment number def extract_number(filename): try: base = os.path.basename(filename) # Handle both "segment_X" and "scene_X" naming conventions if base.startswith('segment_'): number_part = base.split('_')[1].split('.')[0] elif base.startswith('scene_'): number_part = base.split('_')[1].split('.')[0] else: # For other files, extract any number import re numbers = re.findall(r'\d+', base) number_part = numbers[0] if numbers else '0' return int(number_part) except: return float('inf') # Put files without numbers at the end return sorted(files, key=extract_number) else: raise OSError(f"{folder} not found.") def create_srt(text: str, audio_file: AudioFileClip, outfile_name: str, duration: int, chunk_size=5): """ Create SRT subtitle file from text and audio duration. """ words = text.split() chars = " ".join(words) chars_count = len(chars) char_duration = audio_file.duration / chars_count if chars_count > 0 else 0 subs = pysrt.SubRipFile() start_time = duration for i in range(0, len(words), chunk_size): chunk = " ".join(words[i:i + chunk_size]) end_time = start_time + (len(chunk) * char_duration) subtitle = pysrt.SubRipItem( index=len(subs) + 1, start=pysrt.SubRipTime(seconds=start_time), end=pysrt.SubRipTime(seconds=end_time), text=chunk ) subs.append(subtitle) start_time = end_time # Create subtitles directory if it doesn't exist srt_dir = "samples/subtitles/srt" os.makedirs(srt_dir, exist_ok=True) out = os.path.join(srt_dir, f"{outfile_name}.srt") subs.save(out, encoding='utf-8') return out def extract_topic_from_json(file_path): ''' Extract topic from JSON file. ''' try: with open(file_path, 'r', encoding='utf-8') as file: data = json.load(file) topic = data.get('topic', 'No topic found') return topic except FileNotFoundError: print(f"Error: The file {file_path} was not found.") return "Unknown Topic" except json.JSONDecodeError: print(f"Error: The file {file_path} contains invalid JSON.") return "Unknown Topic" except Exception as e: print(f"An unexpected error occurred: {e}") return "Unknown Topic" def extract_audio_from_json(file_path): ''' Extract audio script from JSON file. ''' try: with open(file_path, 'r', encoding='utf-8') as file: data = json.load(file) audio_script = data.get('audio_script', []) return audio_script except FileNotFoundError: print(f"Error: The file {file_path} was not found.") return [] except json.JSONDecodeError: print(f"Error: The file {file_path} contains invalid JSON.") return [] except Exception as e: print(f"An unexpected error occurred: {e}") return [] def json_extract(json_path): ''' Extract audio text from JSON file for subtitles. ''' audio_script = extract_audio_from_json(json_path) if audio_script: audio_data = [] for item in audio_script: if 'text' in item: text = item['text'] audio_data.append(text) return audio_data else: print("No audio script found in the JSON file.") return [] def add_effects(clip): """ Add fade in and fade out effects to the video clip. """ try: clip = clip.with_effects([vfx.FadeIn(duration=1)]) clip = clip.with_effects([vfx.FadeOut(duration=1)]) return clip except Exception as e: print(f"Error adding effects: {e}") return clip def create_intro_clip(background_image_path, duration, topic, font_path): """ Create an intro video clip with a background image and centered text. """ try: check_file_exists(background_image_path) # Create background clip background = ImageClip(background_image_path).with_duration(duration) # Create text clip text_clip = TextClip( text=topic, font_size=50, color='white', font=font_path, stroke_color='black', stroke_width=2 ) # Set text position and duration text_clip = text_clip.with_position('center').with_duration(duration) # Composite video clip final_clip = CompositeVideoClip([background, text_clip]) return final_clip except Exception as e: print(f"Error creating intro clip: {e}") # Create a simple color clip as fallback from moviepy import ColorClip fallback_clip = ColorClip(size=(1920, 1080), color=(0, 0, 0), duration=duration) text_clip = TextClip(text=topic, font_size=30, color='white').with_position('center').with_duration(duration) return CompositeVideoClip([fallback_clip, text_clip]) def create_placeholder_image(width=1920, height=1080, text="No Image", font_path=None, font_size=50, text_color="white", bg_color="black"): """ Creates a placeholder image with the specified dimensions and text. """ try: img = Image.new("RGB", (width, height), bg_color) draw = ImageDraw.Draw(img) if font_path and os.path.exists(font_path): font = ImageFont.truetype(font_path, font_size) else: # Use default font font = ImageFont.load_default() # Calculate text size bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] # Center the text text_position = ((width - text_width) // 2, (height - text_height) // 2) # Draw the text draw.text(text_position, text, font=font, fill=text_color) # Save placeholder placeholder_path = "resources/placeholder.png" os.makedirs(os.path.dirname(placeholder_path), exist_ok=True) img.save(placeholder_path) return placeholder_path except Exception as e: print(f"Error creating placeholder: {e}") # Return a simple black image path return "resources/placeholder.png" def create_video(image_folder: str, audio_folder: str, script_path: str, font_path: str, output_file: str, with_subtitles: bool = False): """ Main function that creates the video. The function works in 3 parts: 1. Checks if the given parameters are correct. 2. If `with_subtitle` flag is set to `False`, creates a video with the images and audio in the given folders. Each image is displayed with the same duration as the corresponding audio file. 3. If the `with_subtitle` flag is set to `True`, embeds subtitles within the video itself, cannot be turned off in video players. Video is compiled using the `compose` method so that if the images are of different aspect ratios /resolutions then the video takes the image with the largest resolution or aspect ratio as the default one and puts black bars with the rest of the non-fitting images. """ check_folder_exists(audio_folder) check_file_exists(script_path) check_file_exists(font_path) # Create a placeholder image if no images are provided if not image_folder or not os.path.exists(image_folder) or not os.listdir(image_folder): placeholder_image = create_placeholder_image(font_path=font_path, text="No Image Available") images = [placeholder_image] * len(get_files(audio_folder, ('.mp3', '.wav'))) else: check_folder_exists(image_folder) images = get_files(image_folder, ('.jpg', '.png')) audio_files = get_files(audio_folder, ('.mp3', '.wav')) subtitles = json_extract(script_path) raw_clips = [] audio_durations = [] Start_duration = 0 # Creating the intro clip and appending it to raw clips path_to_background = "resources/intro/intro.jpg" check_file_exists(path_to_background) topic = extract_topic_from_json(script_path) intro_clip = create_intro_clip(path_to_background, duration=5, topic=topic, font_path=font_path) raw_clips.append(intro_clip) # Create different clips with audio - KEEP IT SIMPLE like your working code for img, audio in zip(images, audio_files): audio_clip = AudioFileClip(audio) # This is the key line that works - simple audio attachment image_clip = ImageClip(img).with_duration(audio_clip.duration).with_audio(audio_clip) audio_durations.append(audio_clip.duration) print(f"Video Clip no. {images.index(img) + 1} successfully created") Start_duration += audio_clip.duration image_clip = add_effects(image_clip) raw_clips.append(image_clip) # Creating the outro clip and appending it to raw clips outro_text = "made with Love by Team EDU-FLICK" outro_clip = create_intro_clip(path_to_background, duration=5, topic=outro_text, font_path=font_path) raw_clips.append(outro_clip) video = concatenate_videoclips(raw_clips, method="compose") if with_subtitles: Start_duration = 5 subtitle_clips = [] chunk_size = 10 for text, duration in zip(subtitles, audio_durations): words = text.split() if len(words) > chunk_size: for i in range(0, len(words), chunk_size): chunk = " ".join(words[i:i + chunk_size]) chunk_duration = duration * (len(chunk.split()) / len(words)) subtitle_clip = TextClip( text=chunk, font=font_path, color='white', bg_color='black', size=(1000, 100), method='caption', text_align="center", horizontal_align="center" ).with_duration(chunk_duration).with_start(Start_duration).with_position('bottom') subtitle_clips.append(subtitle_clip) Start_duration += chunk_duration else: subtitle_clip = TextClip( text=text, font=font_path, color='white', bg_color='black', size=(1000, 100), method='caption', text_align="center", horizontal_align="center" ).with_duration(duration).with_start(Start_duration).with_position('bottom') subtitle_clips.append(subtitle_clip) Start_duration += duration subtitle_clips.insert(0, video) final_video = CompositeVideoClip(subtitle_clips) else: final_video = video # Use the exact same write_videofile parameters as your working code final_video.write_videofile(output_file, fps=24, threads=os.cpu_count()) print(f"Video created successfully: {output_file}") def create_complete_srt(script_folder: str, audio_file_folder: str, outfile_path: str, chunk_size=10): """ Creates a complete SRT file from script and audio files. """ try: script = json_extract(script_folder) audio_files = get_files(audio_file_folder, ('.wav', '.mp3')) audio_clips = [AudioFileClip(x) for x in audio_files] subs = pysrt.SubRipFile() start_time = 5 # Start after intro n = 1 for text, audio_clip in zip(script, audio_clips): duration = audio_clip.duration words = text.split() if len(words) > chunk_size: for i in range(0, len(words), chunk_size): chunk = " ".join(words[i:i + chunk_size]) chunk_duration = duration * (len(chunk.split()) / len(words)) end_time = start_time + chunk_duration subtitle = pysrt.SubRipItem( index=n, start=pysrt.SubRipTime(seconds=int(start_time)), # Convert to int end=pysrt.SubRipTime(seconds=int(end_time)), # Convert to int text=chunk ) subs.append(subtitle) start_time = end_time n += 1 else: chunk_duration = duration end_time = start_time + chunk_duration subtitle = pysrt.SubRipItem( index=n, start=pysrt.SubRipTime(seconds=int(start_time)), # Convert to int end=pysrt.SubRipTime(seconds=int(end_time)), # Convert to int text=text ) subs.append(subtitle) start_time = end_time n += 1 # Ensure output directory exists os.makedirs(os.path.dirname(outfile_path), exist_ok=True) subs.save(outfile_path, encoding='utf-8') print(f"✅ SRT file saved successfully at {outfile_path}") except Exception as e: print(f"❌ Error creating SRT file: {e}") if __name__ == "__main__": script_path = "resources/scripts/script.json" images_path = "resources/images/" audio_path = "resources/audio/" font_path = "resources/font/font.ttf" video_output_path = "resources/video/" subtitle_output_path = "resources/subtitles/" create_video(images_path, audio_path, script_path, font_path, "output/newtons_1st_law.mp4", with_subtitles=True)