Skip to content

Commit

Permalink
Feature Request: Background Music Option for Enhanced Video Engagement
Browse files Browse the repository at this point in the history
…FujiwaraChoki#107: Add `music` ability,
  • Loading branch information
FujiwaraChoki committed Feb 9, 2024
1 parent 6e401b0 commit dd96f7b
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 81 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ ASSEMBLY_AI_API_KEY="" # Optional
TIKTOK_SESSION_ID=""
IMAGEMAGICK_BINARY="" # Download from https://imagemagick.org/script/download.php
PEXELS_API_KEY="" # Get from https://www.pexels.com/api/
OPENAI_API_KEY="" # Get from https://platform.openai.com/docs/quickstart?context=python
OPENAI_API_KEY="" # Get from https://platform.openai.com/docs/quickstart?context=python
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ output/*
images/*
*.zip
*.srt
*.mp4
*.mp3
.history
subtitles/*
/venv
Expand Down
16 changes: 8 additions & 8 deletions Backend/gpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ def generate_response(prompt: str, ai_model: str) -> str:

)

elif ai_model in ['gpt3.5-turbo', 'gpt4']:
elif ai_model in ["gpt3.5-turbo", "gpt4"]:

model_name = "gpt-3.5-turbo" if ai_model == 'gpt3.5-turbo' else "gpt-4-1106-preview"
model_name = "gpt-3.5-turbo" if ai_model == "gpt3.5-turbo" else "gpt-4-1106-preview"

response = openai.ChatCompletion.create(

Expand Down Expand Up @@ -81,7 +81,6 @@ def generate_script(video_subject: str, paragraph_number: int, ai_model: str) ->
"""


# Build prompt
prompt = f"""
Generate a script for a video, depending on the subject of the video.
Expand All @@ -100,7 +99,8 @@ def generate_script(video_subject: str, paragraph_number: int, ai_model: str) ->
Obviously, the script should be related to the subject of the video.
ONLY RETURN THE RAW CONTENT OF THE SCRIPT. DO NOT INCLUDE "VOICEOVER", "NARRATOR" OR SIMILAR INDICATORS OF WHAT SHOULD BE SPOKEN AT THE BEGINNING OF EACH PARAGRAPH OR LINE.
YOU MUST NOT INCLUDE ANY TYPE OF MARKDOWN OR FORMATTING IN THE SCRIPT, NEVER USE A TITLE.
ONLY RETURN THE RAW CONTENT OF THE SCRIPT. DO NOT INCLUDE "VOICEOVER", "NARRATOR" OR SIMILAR INDICATORS OF WHAT SHOULD BE SPOKEN AT THE BEGINNING OF EACH PARAGRAPH OR LINE. YOU MUST NOT MENTION THE PROMPT, OR ANYTHING ABOUT THE SCRIPT ITSELF. ALSO, NEVER TALK ABOUT THE AMOUNT OF PARAGRAPHS OR LINES. JUST WRITE THE SCRIPT.
"""

# Generate script
Expand All @@ -116,17 +116,17 @@ def generate_script(video_subject: str, paragraph_number: int, ai_model: str) ->
response = response.replace("#", "")

# Remove markdown syntax
response = re.sub(r'\[.*\]', '', response)
response = re.sub(r'\(.*\)', '', response)
response = re.sub(r"\[.*\]", "", response)
response = re.sub(r"\(.*\)", "", response)

# Split the script into paragraphs
paragraphs = response.split('\n\n')
paragraphs = response.split("\n\n")

# Select the specified number of paragraphs
selected_paragraphs = paragraphs[:paragraph_number]

# Join the selected paragraphs into a single string
final_script = '\n\n'.join(selected_paragraphs)
final_script = "\n\n".join(selected_paragraphs)

# Print to console the number of paragraphs used
print(colored(f"Number of paragraphs used: {len(selected_paragraphs)}", "green"))
Expand Down
144 changes: 93 additions & 51 deletions Backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from apiclient.errors import HttpError
from flask import Flask, request, jsonify
from moviepy.config import change_settings
import openai


# Load environment variables
Expand Down Expand Up @@ -46,16 +45,30 @@ def generate():
# Clean
clean_dir("../temp/")
clean_dir("../subtitles/")

# Download songs
if use_music:
# Downloads a ZIP file containing popular TikTok Songs
if songs_zip_url:
fetch_songs(songs_zip_url)
else:
# Default to a ZIP file containing popular TikTok Songs
fetch_songs("https://filebin.net/2avx134kdibc4c3q/drive-download-20240209T180019Z-001.zip")

# Parse JSON
data = request.get_json()
paragraph_number = int(data.get('paragraphNumber', 1)) # Default to 1 if not provided
ai_model = data.get('aiModel') # Get the AI model selected by the user

# Get 'useMusic' from the request data and default to False if not provided
use_music = data.get('useMusic', False)

# Get 'automateYoutubeUpload' from the request data and default to False if not provided
automate_youtube_upload = data.get('automateYoutubeUpload', False)

# Get the ZIP Url of the songs
songs_zip_url = data.get('zipUrl')

# Print little information about the video which is to be generated
print(colored("[Video to be generated]", "blue"))
print(colored(" Subject: " + data["videoSubject"], "blue"))
Expand Down Expand Up @@ -86,10 +99,13 @@ def generate():

# Search for a video of the given search term
video_urls = []
# defines how many results it should query and search through

# Defines how many results it should query and search through
it = 15
# defines the minimum duration of each clip

# Defines the minimum duration of each clip
min_dur = 10

# Loop through all search terms,
# and search for a video of the given search term
for search_term in search_terms:
Expand All @@ -104,7 +120,7 @@ def generate():
found_url = search_for_stock_videos(
search_term, os.getenv("PEXELS_API_KEY"), it, min_dur
)
#check for duplicates
# Check for duplicates
for url in found_url:
if url not in video_urls:
video_urls.append(url)
Expand Down Expand Up @@ -149,9 +165,11 @@ def generate():

# Split script into sentences
sentences = script.split(". ")

# Remove empty strings
sentences = list(filter(lambda x: x != "", sentences))
paths = []

# Generate TTS for every sentence
for sentence in sentences:
if not GENERATING:
Expand Down Expand Up @@ -189,54 +207,76 @@ def generate():
print(colored(f"[-] Error generating final video: {e}", "red"))
final_video_path = None

# Start Youtube Uploader
# Check if the CLIENT_SECRETS_FILE exists
client_secrets_file = os.path.abspath("./client_secret.json")
SKIP_YT_UPLOAD = False
if not os.path.exists(client_secrets_file):
SKIP_YT_UPLOAD = True
print(colored("[-] Client secrets file missing. YouTube upload will be skipped.", "yellow"))
print(colored("[-] Please download the client_secret.json from Google Cloud Platform and store this inside the /Backend directory.", "red"))

# Only proceed with YouTube upload if the toggle is True and client_secret.json exists.
if automate_youtube_upload and not SKIP_YT_UPLOAD:
# Define metadata for the video
title, description, keywords = generate_metadata(data["videoSubject"], script, ai_model)

print(colored("[-] Metadata for YouTube upload:", "blue"))
print(colored(" Title: ", "blue"))
print(colored(f" {title}", "blue"))
print(colored(" Description: ", "blue"))
print(colored(f" {description}", "blue"))
print(colored(" Keywords: ", "blue"))
print(colored(f" {', '.join(keywords)}", "blue"))

# Choose the appropriate category ID for your videos
video_category_id = "28" # Science & Technology
privacyStatus = "private" # "public", "private", "unlisted"
video_metadata = {
'video_path': os.path.abspath(f"../temp/{final_video_path}"),
'title': title,
'description': description,
'category': video_category_id,
'keywords': ",".join(keywords),
'privacyStatus': privacyStatus,
}
if automate_youtube_upload:
# Start Youtube Uploader
# Check if the CLIENT_SECRETS_FILE exists
client_secrets_file = os.path.abspath("./client_secret.json")
SKIP_YT_UPLOAD = False
if not os.path.exists(client_secrets_file):
SKIP_YT_UPLOAD = True
print(colored("[-] Client secrets file missing. YouTube upload will be skipped.", "yellow"))
print(colored("[-] Please download the client_secret.json from Google Cloud Platform and store this inside the /Backend directory.", "red"))

# Only proceed with YouTube upload if the toggle is True and client_secret.json exists.
if not SKIP_YT_UPLOAD:
# Define metadata for the video
title, description, keywords = generate_metadata(data["videoSubject"], script, ai_model)

print(colored("[-] Metadata for YouTube upload:", "blue"))
print(colored(" Title: ", "blue"))
print(colored(f" {title}", "blue"))
print(colored(" Description: ", "blue"))
print(colored(f" {description}", "blue"))
print(colored(" Keywords: ", "blue"))
print(colored(f" {', '.join(keywords)}", "blue"))

# Choose the appropriate category ID for your videos
video_category_id = "28" # Science & Technology
privacyStatus = "private" # "public", "private", "unlisted"
video_metadata = {
'video_path': os.path.abspath(f"../temp/{final_video_path}"),
'title': title,
'description': description,
'category': video_category_id,
'keywords': ",".join(keywords),
'privacyStatus': privacyStatus,
}

# Upload the video to YouTube
try:
# Unpack the video_metadata dictionary into individual arguments
video_response = upload_video(
video_path=video_metadata['video_path'],
title=video_metadata['title'],
description=video_metadata['description'],
category=video_metadata['category'],
keywords=video_metadata['keywords'],
privacy_status=video_metadata['privacyStatus']
)
print(f"Uploaded video ID: {video_response.get('id')}")
except HttpError as e:
print(f"An HTTP error {e.resp.status} occurred:\n{e.content}")

if use_music:
# Select a random song
song_path = choose_random_song()

# Add song to video at 30% volume using moviepy
video_clip = VideoFileClip(f"../temp/{final_video_path}")
original_duration = video_clip.duration
original_audio = video_clip.audio
song_clip = AudioFileClip(song_path).set_fps(44100)

# Set the volume of the song to 10% of the original volume
song_clip = song_clip.volumex(0.1).set_fps(44100)

# Add the song to the video
comp_audio = CompositeAudioClip([original_audio, song_clip])
video_clip = video_clip.set_audio(comp_audio)
video_clip = video_clip.set_fps(30)
video_clip = video_clip.set_duration(original_duration)
video_clip.write_videofile(f"../{final_video_path}", threads=2)

# Upload the video to YouTube
try:
# Unpack the video_metadata dictionary into individual arguments
video_response = upload_video(
video_path=video_metadata['video_path'],
title=video_metadata['title'],
description=video_metadata['description'],
category=video_metadata['category'],
keywords=video_metadata['keywords'],
privacy_status=video_metadata['privacyStatus']
)
print(f"Uploaded video ID: {video_response.get('id')}")
except HttpError as e:
print(f"An HTTP error {e.resp.status} occurred:\n{e.content}")

# Let user know
print(colored(f"[+] Video generated: {final_video_path}!", "green"))
Expand Down Expand Up @@ -281,4 +321,6 @@ def cancel():


if __name__ == "__main__":

# Run Flask App
app.run(debug=True, host=HOST, port=PORT)
51 changes: 39 additions & 12 deletions Backend/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import zipfile
import json
import random
import logging
import zipfile
import requests

from termcolor import colored
Expand Down Expand Up @@ -34,29 +36,54 @@ def clean_dir(path: str) -> None:
except Exception as e:
logger.error(f"Error occurred while cleaning directory {path}: {str(e)}")

def fetch_music(url: str) -> None:
def fetch_songs(zip_url: str) -> None:
"""
Downloads music into songs/ directory to use with geneated videos.
Downloads songs into songs/ directory to use with geneated videos.
Args:
url (str): URL to the ZIP File of the music.
zip_url (str): The URL to the zip file containing the songs.
Returns:
None
"""
try:
response = requests.get(url)
logger.info(colored(f" => Fetching songs...", "magenta"))

files_dir = "../Songs"
if not os.path.exists(files_dir):
os.mkdir(files_dir)
logger.info(colored(f"Created directory: {files_dir}", "green"))

# Download songs
response = requests.get(zip_url)

# Save the zip file
with open("../Songs/songs.zip", "wb") as file:
file.write(response.content)
logger.info(colored(f"Downloaded ZIP from {url}.", "green"))

# Unzip
with zipfile.ZipFile("../Songs/songs.zip", "r") as zip_ref:
zip_ref.extractall("../Songs")

logger.info(colored(f"Unzipped songs.zip", "green"))
# Unzip the file
with zipfile.ZipFile("../Songs/songs.zip", "r") as file:
file.extractall("../Songs")

# Remove the zip file
os.remove("../Songs/songs.zip")

logger.info(colored(" => Downloaded Songs to ../Songs.", "green"))

except Exception as e:
logger.error(colored(f"Error occurred while fetching songs: {str(e)}", "red"))

def choose_random_song() -> str:
"""
Chooses a random song from the songs/ directory.
Returns:
str: The path to the chosen song.
"""
try:
songs = os.listdir("../Songs")
song = random.choice(songs)
logger.info(colored(f"Chose song: {song}", "green"))
return f"../Songs/{song}"
except Exception as e:
logger.error(f"Error occurred while fetching music: {str(e)}")
logger.error(colored(f"Error occurred while choosing random song: {str(e)}", "red"))
4 changes: 2 additions & 2 deletions Backend/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def combine_videos(video_paths: List[str], max_duration: int) -> str:

final_clip = concatenate_videoclips(clips)
final_clip = final_clip.set_fps(30)
final_clip.write_videofile(combined_video_path, threads=3)
final_clip.write_videofile(combined_video_path, threads=2)

return combined_video_path

Expand Down Expand Up @@ -222,6 +222,6 @@ def generate_video(combined_video_path: str, tts_path: str, subtitles_path: str)
audio = AudioFileClip(tts_path)
result = result.set_audio(audio)

result.write_videofile("../temp/output.mp4", threads=3)
result.write_videofile("../temp/output.mp4", threads=2)

return "output.mp4"
Loading

0 comments on commit dd96f7b

Please sign in to comment.