├── ChatGPT text to movie.py ├── README.md ├── _utils.py └── story.txt /ChatGPT text to movie.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import srt 4 | import sys 5 | import math 6 | import openai 7 | import openai.error 8 | import logging 9 | import datetime 10 | import requests 11 | import traceback 12 | import subprocess 13 | import numpy as np 14 | 15 | from srt import Subtitle 16 | from typing import List, Optional, Tuple, Union 17 | from _utils import perf_monitor, show_running_message_decorator, hm_sz, hm_time 18 | from gtts import gTTS 19 | from pydub import AudioSegment 20 | from PIL import Image 21 | from skimage import img_as_ubyte 22 | from pydub import AudioSegment 23 | 24 | # Constants 25 | story_filename = "story.txt" 26 | num_ChatGPT_images = 1 27 | size_ChatGPT_image = "512x512" 28 | size_ChatGPT_image = "1024x1024" 29 | frame_rate = 30 30 | 31 | output_dir = 'output' 32 | if not os.path.exists(output_dir): 33 | os.makedirs(output_dir) 34 | 35 | @perf_monitor 36 | @show_running_message_decorator 37 | def load_story_from_file(filename: str) -> Optional[List[str]]: 38 | ''' load text line from filename ''' 39 | msj = sys._getframe().f_code.co_name 40 | 41 | if not os.path.isfile(filename): 42 | print(f"Story file '{filename}' not found. Please provide a valid story file.") 43 | filename = input("Enter the path to the story file: ") 44 | 45 | if not os.path.isfile(filename): 46 | print("Invalid file path. Please make sure the file exists and try again.") 47 | return None 48 | 49 | with open(filename, "r") as f: 50 | story = [line.strip() for line in f if line.strip()] # Ignore empty lines or lines with only whitespace 51 | 52 | return story 53 | 54 | 55 | @perf_monitor 56 | @show_running_message_decorator 57 | def generate_and_download_image(prompt: str, image_file: str, output_dir: str) -> Optional[str]: 58 | '''Generate an image using OpenAI's DALL-E API and download it to a local file.''' 59 | msj = sys._getframe().f_code.co_name 60 | # logging.info(f"{msj} Text: {prompt}") 61 | 62 | try: 63 | response = openai.Image.create( 64 | prompt=prompt, 65 | n=num_ChatGPT_images, 66 | size=size_ChatGPT_image, 67 | response_format="url", 68 | ) 69 | image_url = response["data"][0]["url"] 70 | 71 | # Download the image 72 | logging.info(f"Download from: {image_url} to File {image_file}") 73 | 74 | response = requests.get(image_url) 75 | if not response : 76 | raise (" No image created") 77 | 78 | with open(image_file, "wb") as f: 79 | f.write(response.content) 80 | logging.info(f" image done {image_file}") 81 | 82 | return image_file 83 | 84 | except openai.error.InvalidRequestError as e: 85 | msj = f"Request for prompt '{prompt}' was rejected: {e}" 86 | print(msj) 87 | logging.error(msj) 88 | return None 89 | except Exception as e: 90 | msj = f"An error occurred while generating an image for '{prompt}': {e}\n{traceback.format_exc()}" 91 | print (msj) 92 | logging.error(msj) 93 | return None 94 | 95 | @perf_monitor 96 | @show_running_message_decorator 97 | def generate_audio(text: str, out_file: str): 98 | ''' Audio from text ''' 99 | msj = sys._getframe().f_code.co_name 100 | msj += f" Audio from {text} to {out_file}" 101 | logging.info(msj) 102 | print (msj) 103 | try: 104 | tts = gTTS(text=text, lang='en') 105 | tts.save(out_file) 106 | audio = AudioSegment.from_mp3(out_file) 107 | au_len = math.ceil(audio.duration_seconds) 108 | except Exception as e: 109 | msj = f"An error occurred while generating audio from '{text}': {e}\n{traceback.format_exc()}" 110 | print (msj) 111 | 112 | return out_file, au_len 113 | 114 | @perf_monitor 115 | @show_running_message_decorator 116 | def combine_all_audio(audio_files : str, output_dir: str) -> str : 117 | ''' combine audiofiles into one ''' 118 | msj = sys._getframe().f_code.co_name 119 | # Combine audio files 120 | merged_audio_file = f"{output_dir}" 121 | merged_audio = AudioSegment.empty() 122 | for audio_file in audio_files: 123 | audio_segment = AudioSegment.from_file(audio_file) 124 | merged_audio = merged_audio + audio_segment 125 | merged_audio.export(merged_audio_file, format="mp3") 126 | msj += f" Merged audio file: {merged_audio_file}" 127 | logging.info( msj ) 128 | print( msj ) 129 | return merged_audio_file 130 | 131 | @perf_monitor 132 | def combine_all_subtl(subtitle_entries, output_file): 133 | ''' Create subtitle bile from subtitiles''' 134 | msj = sys._getframe().f_code.co_name 135 | 136 | msj += f" subtitle_entries -> File: {output_file}" 137 | print ( msj ) 138 | 139 | subtitles = [] 140 | start_time = datetime.timedelta() 141 | for index, entry in enumerate(subtitle_entries): 142 | duration = datetime.timedelta(seconds=entry["duration"]) 143 | end_time = start_time + duration 144 | content = sanitize_txt(entry['text']) 145 | subtitle = Subtitle(index, start_time, end_time, content) 146 | subtitles.append(subtitle) 147 | start_time = end_time 148 | 149 | with open(output_file, "w") as f: 150 | f.write(srt.compose(subtitles)) 151 | 152 | return output_file 153 | 154 | @perf_monitor 155 | def sanitize_txt(content): 156 | """ 157 | Replace any illegal characters with spaces and remove extra spaces. 158 | """ 159 | msj = sys._getframe().f_code.co_name 160 | 161 | return re.sub(r'[^\w\s]', ' ', content).strip() 162 | 163 | @perf_monitor 164 | @show_running_message_decorator 165 | def create_video (video_files : str ='.', aud_all_merged: str ='.', srt_all_merged: str ='.', output_file: str= 'NUL' ) : 166 | ''' create video''' 167 | msj = sys._getframe().f_code.co_name 168 | msj += f" Create Video" 169 | logging.info(msj) 170 | print(msj) 171 | try: 172 | cmd = f"ffmpeg -report -framerate {frame_rate} -i {output_dir}/morphed_image_%04d.png -i {aud_all_merged} -i {srt_all_merged} \ 173 | -map 0:v -c:v libx265 -preset slow -crf 25 \ 174 | -map 1:a -c:a copy -metadata:s:a:0 language=eng \ 175 | -map 2:s -c:s mov_text -metadata:s:s:0 language=eng -disposition:s:s:0 default+forced\ 176 | -y {output_file}" 177 | 178 | # XXX: -vf subtitles={srt_all_merged} could burn the subtitle into the movie \ # XXX: 179 | 180 | # print(f"\nExecuting command: {cmd}\n") 181 | # print(f"Input file names: {output_dir}/morphed_image_*.png, {aud_all_merged}, {srt_all_merged}") 182 | subprocess.run(cmd, shell=True, check=True) 183 | msj = f"Video {output_file} Done" 184 | print (msj) 185 | logging.info (msj) 186 | return True 187 | except Exception as e: 188 | msj = f"An error occurred runninf ffmpeg: {e}\n{traceback.format_exc()}" 189 | logging.error(msj) 190 | print (msj) 191 | 192 | @perf_monitor 193 | @show_running_message_decorator 194 | def morph_images(image1: str, image2: str, steps: int = 10, output_dir: str = ".", start_index: int = 0) -> Optional[List[str]]: 195 | ''' morph images in number of steps''' 196 | msj = sys._getframe().f_code.co_name 197 | msj += f" from: {image1} to: {image2} in {steps} Steps" 198 | logging.info(msj) 199 | print(msj) 200 | try: 201 | with Image.open(image1) as img1, Image.open(image2) as img2: 202 | img1_array = np.array(img1, dtype=np.float32) / 255.0 203 | img2_array = np.array(img2, dtype=np.float32) / 255.0 204 | 205 | morphed_images = [] 206 | for cnt in range(steps + 1): 207 | alpha = cnt / steps 208 | morphed_image = (1 - alpha) * img1_array + alpha * img2_array 209 | 210 | # Save the morphed image to the disk 211 | val = start_index + cnt 212 | filename = f"{output_dir}/morphed_image_{val:04d}.png" 213 | img = Image.fromarray(img_as_ubyte(morphed_image)) 214 | img.save(filename) 215 | morphed_images.append(filename) 216 | 217 | return morphed_images, val +1 218 | 219 | except Exception as e: 220 | logging.error(f"An error occurred while morphing images: {e}\n{traceback.format_exc()}") 221 | return [] 222 | 223 | @perf_monitor 224 | def main(story_file): 225 | images = [] 226 | audio_files = [] 227 | subtitle_entries = [] 228 | start_time = 0 229 | 230 | for cnt, line in enumerate(story_file): 231 | 232 | image_file = f"{output_dir}/image_{cnt:04d}.png" 233 | print(f"\n{cnt} Line {line} -> {image_file}") 234 | 235 | ## XXX: Generate and download image # XXX: 236 | image_file = generate_and_download_image(line, image_file , output_dir) 237 | images.append(image_file) 238 | 239 | # Generate audio measure duration 240 | audio_file_name = f"{output_dir}/{cnt:04d}_audio.mp3" 241 | audio_file, duration = generate_audio(line, audio_file_name) 242 | audio_files.append(audio_file) 243 | 244 | # subtitle entries 245 | end_time = start_time + duration 246 | subtitle_entries.append({ 247 | "start": f"{start_time:.3f}", 248 | "end": f"{end_time:.3f}", 249 | "text": line, 250 | "duration": duration }) # Add duration to the dictionary 251 | start_time = end_time 252 | 253 | # Morph images 254 | morphed_image_sequences = [] 255 | morph_steps = duration *frame_rate 256 | if cnt == 0 : 257 | start_index = 0 258 | if cnt > 0: 259 | morphed_images, start_index = morph_images(images[cnt - 1], images[cnt], morph_steps, output_dir, start_index ) 260 | morphed_image_sequences.extend(morphed_images) 261 | 262 | print(f"Total all: {len(morphed_image_sequences)}") 263 | 264 | # Merge audio files 265 | print(f"\n{len(audio_files)} Audio files") 266 | merged_audio = f"{output_dir}/merged_audio.mp3" 267 | aud_all_merged = combine_all_audio(audio_files, merged_audio) 268 | 269 | # Create subtitle file 270 | print(f"\n{len(subtitle_entries)} Srt files") 271 | srt_file = f"{output_dir}/subtitles.srt" 272 | srt_all_merged = combine_all_subtl(subtitle_entries, srt_file) 273 | 274 | # Add audio and subtitles to video 275 | video_audio_srt = f"{output_dir}/video_audio_srt.mp4" 276 | all_good = create_video ( f"{output_dir}/morphed_image_%04d.png", aud_all_merged, srt_all_merged, video_audio_srt ) 277 | 278 | if __name__ == "__main__": 279 | # XXX: Setup the loggigng file 280 | name, _ = os.path.splitext(sys.argv[0]) 281 | name += '_.log' 282 | # Set up logging to file 283 | logging.basicConfig( 284 | level=logging.INFO, 285 | # level=logging.DEBUG, 286 | filename=(name), filemode='w', 287 | format='%(asctime)s %(levelname)s %(message)s', 288 | datefmt='%d-%H:%M:%S') 289 | 290 | story_file = load_story_from_file(story_filename) 291 | main(story_file) 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Story-to-Video 2 | Create a Movie "animation" vith audio and Subtitle from text: Story to Video Generator 3 | 4 | This program generates a video from text using OpenAI's ChatGPT to create a sequence of images and gTTS (Google Text-to-Speech) to produce audio. The images and audio are then combined to create the final video. 5 | 6 | 1) Installation 7 | 8 | - Ensure you have Python 3.6 or higher installed on your system. 9 | - Install the required libraries by running the following command in your terminal or command prompt: 10 | 11 | pip install openai requests gtts pydub scikit-image pillow numpy psutil 12 | 13 | - Install FFmpeg, which is used for video processing. Visit the official FFmpeg website for installation instructions for your specific operating system. 14 | 15 | https://ffmpeg.org/download.html 16 | 17 | 2) Usage 18 | 19 | Run the Python script and follow the prompts to input the text file by default story.txt containing the story you would like to convert to video. 20 | The program will generate images and audio based on your input and combine them into a video. 21 | The Image files generated by oppen.ai are downloaded to thworking directory while the intermediate files and the resulting video to ../output 22 | 23 | 3) Enhancements and Customizations 24 | You can improve the program or customize it to suit your needs by: 25 | 26 | - Add subtitles with the text synchronized to audio 27 | - Using alternative image generation models, such as StyleGAN, BigGAN, or DALLE-2, to create more diverse or higher-quality images. 28 | - Incorporating different text-to-speech (TTS) libraries, such as Tacotron 2 or Mozilla's TTS, to generate audio with varied voices, accents, or intonations. 29 | - Adjusting the program parameters (e.g., frame rate, video duration, audio quality) to optimize the output video. 30 | 31 | 4) Contributing 32 | Feel free to contribute to this project by suggesting improvements, reporting bugs, or implementing new features. You can submit your changes via pull requests or open issues on the project repository. 33 | 34 | Enjoy your text-to-video conversion! 35 | -------------------------------------------------------------------------------- /_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import time 5 | import psutil 6 | import logging 7 | import threading 8 | import tracemalloc 9 | 10 | from typing import Union 11 | from functools import wraps 12 | 13 | # DECORATORS 14 | def perf_monitor(func): 15 | """ Measure performance of a function """ 16 | @wraps(func) 17 | def wrapper(*args, **kwargs): 18 | strt_time = time.perf_counter() 19 | cpu_percent_prev = psutil.cpu_percent(interval=0.05, percpu=False) 20 | tracemalloc.start() 21 | try: 22 | return func(*args, **kwargs) 23 | except Exception as e: 24 | logging.exception(f"Exception in {func.__name__}: {e}", exc_info=True, stack_info=True) 25 | finally: 26 | current, peak = tracemalloc.get_traced_memory() 27 | tracemalloc.stop() 28 | cpu_percent = psutil.cpu_percent(interval=None, percpu=False) 29 | cpu_percnt = cpu_percent - cpu_percent_prev 30 | end_time = time.perf_counter() 31 | duration = end_time - strt_time 32 | msj = f"{func.__name__}\t\tUsed {abs(cpu_percnt):>5.1f} % CPU: {hm_time(duration)}\t Mem: [avr:{hm_sz(current):>8}, max:{hm_sz(peak):>8}]\t({func.__doc__})" 33 | logging.info(msj) 34 | return wrapper 35 | 36 | def show_running_message_decorator(func): 37 | @wraps(func) 38 | def wrapper(*args, **kwargs): 39 | message = f" {func.__name__} running" 40 | 41 | def progress_indicator(): 42 | sys.stdout.write(message) 43 | while not progress_indicator.stop: 44 | for pattern in "|/-o+\\": 45 | sys.stdout.write(f"\r{message} {pattern}") 46 | sys.stdout.flush() 47 | time.sleep(0.1) 48 | sys.stdout.write(f"\r{message} Done!\n") 49 | sys.stdout.flush() 50 | 51 | progress_indicator.stop = False 52 | progress_thread = threading.Thread(target=progress_indicator) 53 | progress_thread.start() 54 | 55 | try: 56 | result = func(*args, **kwargs) 57 | finally: 58 | progress_indicator.stop = True 59 | progress_thread.join() 60 | 61 | return result 62 | return wrapper 63 | 64 | ''' 65 | # Example usage 66 | @show_running_message_decorator 67 | def some_long_running_function(): 68 | time.sleep(5) 69 | 70 | some_long_running_function() 71 | ''' 72 | 73 | def measure_cpu_time(func): 74 | def wrapper(*args, **kwargs): 75 | start_time = time.time() 76 | cpu_percent = psutil.cpu_percent(interval=None, percpu=True) 77 | result = func(*args, **kwargs) 78 | elapsed_time = time.time() - start_time 79 | cpu_percent = [p - c for p, c in zip(psutil.cpu_percent(interval=None, percpu=True), cpu_percent)] 80 | print(f"Function {func.__name__} used {sum(cpu_percent)/len(cpu_percent)}% CPU over {elapsed_time:.2f} seconds") 81 | return result 82 | return wrapper 83 | 84 | 85 | def logit(logfile='out.log', de_bug=False): 86 | def logging_decorator(func): 87 | @wraps(func) 88 | def wrapper(*args, **kwargs): 89 | result = func(*args, **kwargs) 90 | with open(logfile, 'a') as f: 91 | if len(kwargs) > 0: 92 | f.write(f"\n{func.__name__}{args} {kwargs} = {result}\n") 93 | else: 94 | f.write(f"\n{func.__name__}{args} = {result}\n") 95 | if de_bug: 96 | if len(kwargs) > 0: 97 | print(f"{func.__name__}{args} {kwargs} = {result}") 98 | else: 99 | print(f"{func.__name__}{args} = {result}") 100 | return result 101 | return wrapper 102 | return logging_decorator 103 | 104 | 105 | def handle_exception(func): 106 | """Decorator to handle exceptions.""" 107 | @wraps(func) 108 | def wrapper(*args, **kwargs): 109 | try: 110 | return func(*args, **kwargs) 111 | except Exception as e: 112 | print(f"Exception in {func.__name__}: {e}") 113 | logging.exception(f"Exception in {func.__name__}: {e}",exc_info=True, stack_info=True) 114 | # sys.exit(1) 115 | except TypeError : 116 | print(f"{func.__name__} wrong data types") 117 | except IOError: 118 | print("Could not write to file.") 119 | except : 120 | print("Someting Else?") 121 | else: 122 | print("No Exceptions") 123 | finally: 124 | logging.error("Error: ", exc_info=True) 125 | logging.error("uncaught exception: %s", traceback.format_exc()) 126 | return wrapper 127 | 128 | 129 | def measure_cpu_utilization(func): 130 | """Measure CPU utilization, number of cores used, and their capacity.""" 131 | @wraps(func) 132 | def wrapper(*args, **kwargs): 133 | cpu_count = psutil.cpu_count(logical=True) 134 | strt_time = time.monotonic() 135 | cpu_prcnt = psutil.cpu_percent(interval=0.1, percpu=True) 136 | result = func(*args, **kwargs) 137 | end_time = time.monotonic() 138 | cpu_percnt = sum(cpu_prcnt) / cpu_count 139 | return result, cpu_percnt, cpu_prcnt 140 | return wrapper 141 | 142 | def log_exceptions(func): 143 | """Log exceptions that occur within a function.""" 144 | @wraps(func) 145 | def wrapper(*args, **kwargs): 146 | try: 147 | return func(*args, **kwargs) 148 | except Exception as e: 149 | print(f"Exception in {func.__name__}: {e}") 150 | logging.exception(f"Exception in {func.__name__}: {e}",exc_info=True, stack_info=True) 151 | return wrapper 152 | 153 | def measure_execution_time(func): 154 | """Measure the execution time of a function.""" 155 | @wraps(func) 156 | def wrapper(*args, **kwargs): 157 | strt_time = time.perf_counter() 158 | result = func(*args, **kwargs) 159 | end_time = time.perf_counter() 160 | duration = end_time - strt_time 161 | print(f"{func.__name__}: Execution time: {duration:.5f} sec") 162 | return result 163 | return wrapper 164 | 165 | def measure_memory_usage(func): 166 | """Measure the memory usage of a function.""" 167 | @wraps(func) 168 | def wrapper(*args, **kwargs): 169 | tracemalloc.start() 170 | result = func(*args, **kwargs) 171 | current, peak = tracemalloc.get_traced_memory() 172 | print(f"{func.__name__}: Mem usage: {current / 10**6:.6f} MB (avg), {peak / 10**6:.6f} MB (peak)") 173 | tracemalloc.stop() 174 | return result 175 | return wrapper 176 | 177 | def performance_check(func): 178 | """Measure performance of a function""" 179 | @log_exceptions 180 | @measure_execution_time 181 | @measure_memory_usage 182 | @measure_cpu_utilization 183 | @wraps(func) 184 | def wrapper(*args, **kwargs): 185 | return func(*args, **kwargs) 186 | return wrapper 187 | 188 | def temperature (): 189 | sensors = psutil.sensors_temperatures() 190 | for name, entries in sensors.items(): 191 | print(f"{name}:") 192 | for entry in entries: 193 | print(f" {entry.label}: {entry.current}°C") 194 | 195 | def perf_monitor_temp(func): 196 | """ Measure performance of a function """ 197 | @wraps(func) 198 | def wrapper(*args, **kwargs): 199 | strt_time = time.perf_counter() 200 | cpu_percent_prev = psutil.cpu_percent(interval=0.05, percpu=False) 201 | tracemalloc.start() 202 | try: 203 | return func(*args, **kwargs) 204 | except Exception as e: 205 | logging.exception(f"Exception in {func.__name__}: {e}",exc_info=True, stack_info=True) 206 | finally: 207 | current, peak = tracemalloc.get_traced_memory() 208 | tracemalloc.stop() 209 | cpu_percent = psutil.cpu_percent(interval=None, percpu=False) 210 | cpu_percnt = cpu_percent - cpu_percent_prev 211 | 212 | # New code to measure CPU temperature 213 | cpu_temp = psutil.sensors_temperatures().get('coretemp')[0].current 214 | print(f"CPU temperature: {cpu_temp}°C") 215 | 216 | end_time = time.perf_counter() 217 | duration = end_time - strt_time 218 | msj = f"{func.__name__}\t\tUsed {abs(cpu_percnt):>5.1f} % CPU: {hm_time(duration)}\t Mem: [avr:{hm_sz(current):>8}, max:{hm_sz(peak):>8}]\t({func.__doc__})" 219 | logging.info(msj) 220 | return wrapper 221 | 222 | ##>>============-------------------< End >------------------==============<<## 223 | 224 | # CLASES 225 | # XXX: https://shallowsky.com/blog/programming/python-tee.html 226 | 227 | class Tee: 228 | ''' implement the Linux Tee function ''' 229 | 230 | def __init__(self, *targets): 231 | self.targets = targets 232 | 233 | def __del__(self): 234 | for target in self.targets: 235 | if target not in (sys.stdout, sys.stderr): 236 | target.close() 237 | 238 | def write(self, obj): 239 | for target in self.targets: 240 | try: 241 | target.write(obj) 242 | target.flush() 243 | except Exception: 244 | pass 245 | 246 | def flush(self): 247 | pass 248 | 249 | class RunningAverage: 250 | ''' Compute the running averaga of a value ''' 251 | 252 | def __init__(self): 253 | self.n = 0 254 | self.avg = 0 255 | 256 | def update(self, x): 257 | self.avg = (self.avg * self.n + x) / (self.n + 1) 258 | self.n += 1 259 | 260 | def get_avg(self): 261 | return self.avg 262 | 263 | def reset(self): 264 | self.n = 0 265 | self.avg = 0 266 | ##>>============-------------------< End >------------------==============<<## 267 | ## Functions 268 | 269 | def hm_sz(numb: Union[str, int, float], type: str = "B") -> str: 270 | '''convert file size to human readable format''' 271 | numb = float(numb) 272 | try: 273 | if numb < 1024.0: 274 | return f"{numb} {type}" 275 | for unit in ['','K','M','G','T','P','E']: 276 | if numb < 1024.0: 277 | return f"{numb:.2f} {unit}{type}" 278 | numb /= 1024.0 279 | return f"{numb:.2f} {unit}{type}" 280 | except Exception as e: 281 | logging.exception(f"Error {e}", exc_info=True, stack_info=True) 282 | print (e) 283 | # traceback.print_exc() 284 | ##==============------------------- End -------------------==============## 285 | 286 | def hm_time(timez: float) -> str: 287 | '''Print time as years, months, weeks, days, hours, min, sec''' 288 | units = {'year': 31536000, 289 | 'month': 2592000, 290 | 'week': 604800, 291 | 'day': 86400, 292 | 'hour': 3600, 293 | 'min': 60, 294 | 'sec': 1, 295 | } 296 | if timez < 0: 297 | return "Error negative" 298 | elif timez == 0 : 299 | return "Zero" 300 | elif timez < 0.001: 301 | return f"{timez * 1000:.3f} ms" 302 | elif timez < 60: 303 | return f"{timez:>5.3f} sec{'s' if timez > 1 else ''}" 304 | else: 305 | frmt = [] 306 | for unit, seconds_per_unit in units.items() : 307 | value = timez // seconds_per_unit 308 | if value != 0: 309 | frmt.append(f"{int(value)} {unit}{'s' if value > 1 else ''}") 310 | timez %= seconds_per_unit 311 | return ", ".join(frmt[:-1]) + " and " + frmt[-1] if len(frmt) > 1 else frmt[0] if len(frmt) == 1 else "0 sec" 312 | 313 | 314 | ##>>============-------------------< End >------------------==============<<## 315 | 316 | def file_size(path): 317 | # Return file/dir size (MB) 318 | mb = 1 << 20 # bytes to MiB (1024 ** 2) 319 | path = Path(path) 320 | if path.is_file(): 321 | return path.stat().st_size / mb 322 | elif path.is_dir(): 323 | return sum(f.stat().st_size for f in path.glob('**/*') if f.is_file()) / mb 324 | else: 325 | return 0.0 326 | ##>>============-------------------< End >------------------==============<<## 327 | -------------------------------------------------------------------------------- /story.txt: -------------------------------------------------------------------------------- 1 | In a cozy cafe, a delicious cup of boba tea sits on a vibrant red table. 2 | A charming white cat gazes at Baby Yoda through the window with curiosity. 3 | The adorable white cat grins as Baby Yoda and cries out for assistance, clutching at a bottle of milk tightly in his tiny hand. 4 | A caring teacher rushes to the rescue, but arrives a moment too late to offer the boba tea to Baby Yoda. 5 | The boba tea spills onto the floor, causing the white cat to shriek in surprise. The teacher is startled by the commotion, while Baby Yoda bursts into laughter at the unfolding scene. 6 | The end. 7 | --------------------------------------------------------------------------------