|
import os |
|
import uuid |
|
|
|
import requests |
|
import srt_equalizer |
|
import assemblyai as aai |
|
|
|
from typing import List |
|
from moviepy.editor import * |
|
from termcolor import colored |
|
from dotenv import load_dotenv |
|
from datetime import timedelta |
|
from moviepy.video.fx.all import crop |
|
from moviepy.video.tools.subtitles import SubtitlesClip |
|
|
|
load_dotenv("../.env") |
|
|
|
ASSEMBLY_AI_API_KEY = os.getenv("ASSEMBLY_AI_API_KEY") |
|
|
|
|
|
def save_video(video_url: str, directory: str = "../temp") -> str: |
|
""" |
|
Saves a video from a given URL and returns the path to the video. |
|
|
|
Args: |
|
video_url (str): The URL of the video to save. |
|
directory (str): The path of the temporary directory to save the video to |
|
|
|
Returns: |
|
str: The path to the saved video. |
|
""" |
|
video_id = uuid.uuid4() |
|
video_path = f"{directory}/{video_id}.mp4" |
|
with open(video_path, "wb") as f: |
|
f.write(requests.get(video_url).content) |
|
|
|
return video_path |
|
|
|
|
|
def __generate_subtitles_assemblyai(audio_path: str, voice: str) -> str: |
|
""" |
|
Generates subtitles from a given audio file and returns the path to the subtitles. |
|
|
|
Args: |
|
audio_path (str): The path to the audio file to generate subtitles from. |
|
|
|
Returns: |
|
str: The generated subtitles |
|
""" |
|
|
|
language_mapping = { |
|
"br": "pt", |
|
"id": "en", |
|
"jp": "ja", |
|
"kr": "ko", |
|
} |
|
|
|
if voice in language_mapping: |
|
lang_code = language_mapping[voice] |
|
else: |
|
lang_code = voice |
|
|
|
aai.settings.api_key = ASSEMBLY_AI_API_KEY |
|
config = aai.TranscriptionConfig(language_code=lang_code) |
|
transcriber = aai.Transcriber(config=config) |
|
transcript = transcriber.transcribe(audio_path) |
|
subtitles = transcript.export_subtitles_srt() |
|
|
|
return subtitles |
|
|
|
|
|
def __generate_subtitles_locally(sentences: List[str], audio_clips: List[AudioFileClip]) -> str: |
|
""" |
|
Generates subtitles from a given audio file and returns the path to the subtitles. |
|
|
|
Args: |
|
sentences (List[str]): all the sentences said out loud in the audio clips |
|
audio_clips (List[AudioFileClip]): all the individual audio clips which will make up the final audio track |
|
Returns: |
|
str: The generated subtitles |
|
""" |
|
|
|
def convert_to_srt_time_format(total_seconds): |
|
|
|
if total_seconds == 0: |
|
return "0:00:00,0" |
|
return str(timedelta(seconds=total_seconds)).rstrip('0').replace('.', ',') |
|
|
|
start_time = 0 |
|
subtitles = [] |
|
|
|
for i, (sentence, audio_clip) in enumerate(zip(sentences, audio_clips), start=1): |
|
duration = audio_clip.duration |
|
end_time = start_time + duration |
|
|
|
|
|
subtitle_entry = f"{i}\n{convert_to_srt_time_format(start_time)} --> {convert_to_srt_time_format(end_time)}\n{sentence}\n" |
|
subtitles.append(subtitle_entry) |
|
|
|
start_time += duration |
|
|
|
return "\n".join(subtitles) |
|
|
|
|
|
def generate_subtitles(audio_path: str, sentences: List[str], audio_clips: List[AudioFileClip], voice: str) -> str: |
|
""" |
|
Generates subtitles from a given audio file and returns the path to the subtitles. |
|
|
|
Args: |
|
audio_path (str): The path to the audio file to generate subtitles from. |
|
sentences (List[str]): all the sentences said out loud in the audio clips |
|
audio_clips (List[AudioFileClip]): all the individual audio clips which will make up the final audio track |
|
|
|
Returns: |
|
str: The path to the generated subtitles. |
|
""" |
|
|
|
def equalize_subtitles(srt_path: str, max_chars: int = 10) -> None: |
|
|
|
srt_equalizer.equalize_srt_file(srt_path, srt_path, max_chars) |
|
|
|
|
|
subtitles_path = f"../subtitles/{uuid.uuid4()}.srt" |
|
|
|
if ASSEMBLY_AI_API_KEY is not None and ASSEMBLY_AI_API_KEY != "": |
|
print(colored("[+] Creating subtitles using AssemblyAI", "blue")) |
|
subtitles = __generate_subtitles_assemblyai(audio_path, voice) |
|
else: |
|
print(colored("[+] Creating subtitles locally", "blue")) |
|
subtitles = __generate_subtitles_locally(sentences, audio_clips) |
|
|
|
|
|
|
|
|
|
with open(subtitles_path, "w") as file: |
|
file.write(subtitles) |
|
|
|
|
|
equalize_subtitles(subtitles_path) |
|
|
|
print(colored("[+] Subtitles generated.", "green")) |
|
|
|
return subtitles_path |
|
|
|
|
|
def combine_videos(video_paths: List[str], max_duration: int, max_clip_duration: int, threads: int) -> str: |
|
""" |
|
Combines a list of videos into one video and returns the path to the combined video. |
|
|
|
Args: |
|
video_paths (List): A list of paths to the videos to combine. |
|
max_duration (int): The maximum duration of the combined video. |
|
max_clip_duration (int): The maximum duration of each clip. |
|
threads (int): The number of threads to use for the video processing. |
|
|
|
Returns: |
|
str: The path to the combined video. |
|
""" |
|
video_id = uuid.uuid4() |
|
combined_video_path = f"../temp/{video_id}.mp4" |
|
|
|
|
|
req_dur = max_duration / len(video_paths) |
|
|
|
print(colored("[+] Combining videos...", "blue")) |
|
print(colored(f"[+] Each clip will be maximum {req_dur} seconds long.", "blue")) |
|
|
|
clips = [] |
|
tot_dur = 0 |
|
|
|
while tot_dur < max_duration: |
|
for video_path in video_paths: |
|
clip = VideoFileClip(video_path) |
|
clip = clip.without_audio() |
|
|
|
if (max_duration - tot_dur) < clip.duration: |
|
clip = clip.subclip(0, (max_duration - tot_dur)) |
|
|
|
elif req_dur < clip.duration: |
|
clip = clip.subclip(0, req_dur) |
|
clip = clip.set_fps(30) |
|
|
|
|
|
|
|
if round((clip.w/clip.h), 4) < 0.5625: |
|
clip = crop(clip, width=clip.w, height=round(clip.w/0.5625), \ |
|
x_center=clip.w / 2, \ |
|
y_center=clip.h / 2) |
|
else: |
|
clip = crop(clip, width=round(0.5625*clip.h), height=clip.h, \ |
|
x_center=clip.w / 2, \ |
|
y_center=clip.h / 2) |
|
clip = clip.resize((1080, 1920)) |
|
|
|
if clip.duration > max_clip_duration: |
|
clip = clip.subclip(0, max_clip_duration) |
|
|
|
clips.append(clip) |
|
tot_dur += clip.duration |
|
|
|
final_clip = concatenate_videoclips(clips) |
|
final_clip = final_clip.set_fps(30) |
|
final_clip.write_videofile(combined_video_path, threads=threads) |
|
|
|
return combined_video_path |
|
|
|
|
|
def generate_video(combined_video_path: str, tts_path: str, subtitles_path: str, threads: int, subtitles_position: str, text_color : str) -> str: |
|
""" |
|
This function creates the final video, with subtitles and audio. |
|
|
|
Args: |
|
combined_video_path (str): The path to the combined video. |
|
tts_path (str): The path to the text-to-speech audio. |
|
subtitles_path (str): The path to the subtitles. |
|
threads (int): The number of threads to use for the video processing. |
|
subtitles_position (str): The position of the subtitles. |
|
|
|
Returns: |
|
str: The path to the final video. |
|
""" |
|
|
|
generator = lambda txt: TextClip( |
|
txt, |
|
font="../fonts/bold_font.ttf", |
|
fontsize=100, |
|
color=text_color, |
|
stroke_color="black", |
|
stroke_width=5, |
|
) |
|
|
|
|
|
horizontal_subtitles_position, vertical_subtitles_position = subtitles_position.split(",") |
|
|
|
|
|
subtitles = SubtitlesClip(subtitles_path, generator) |
|
result = CompositeVideoClip([ |
|
VideoFileClip(combined_video_path), |
|
subtitles.set_pos((horizontal_subtitles_position, vertical_subtitles_position)) |
|
]) |
|
|
|
|
|
audio = AudioFileClip(tts_path) |
|
result = result.set_audio(audio) |
|
|
|
result.write_videofile("../temp/output.mp4", threads=threads or 2) |
|
|
|
return "output.mp4" |
|
|