SilentProgrammer commited on
Commit
e71d833
·
verified ·
1 Parent(s): 0991432

Upload 18 files

Browse files
.env.example ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See EnvironmentVariables.md for more information.
2
+
3
+ # Necessary API Keys
4
+ # -------------------
5
+
6
+ # TikTok Session ID
7
+ # Obtain your session ID by logging into TikTok and copying the sessionid cookie.
8
+ TIKTOK_SESSION_ID=""
9
+
10
+ # ImageMagick Binary Path
11
+ # Download ImageMagick from https://imagemagick.org/script/download.php
12
+ IMAGEMAGICK_BINARY=""
13
+
14
+ # Pexels API Key
15
+ # Register at https://www.pexels.com/api/ to get your API key.
16
+ PEXELS_API_KEY=""
17
+
18
+ # Optional API Keys
19
+ # -----------------
20
+
21
+ # OpenAI API Key
22
+ # Visit https://openai.com/api/ for details on obtaining an API key.
23
+ OPENAI_API_KEY=""
24
+
25
+ # AssemblyAI API Key
26
+ # Sign up at https://www.assemblyai.com/ to receive an API key.
27
+ ASSEMBLY_AI_API_KEY=""
28
+
29
+ # Google API Key
30
+ # Generate your API key through https://makersuite.google.com/app/apikey
31
+ GOOGLE_API_KEY=""
32
+
.gitignore ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .env
3
+ temp/*
4
+ sounds/*
5
+ output/*
6
+ images/*
7
+ *.zip
8
+ *.srt
9
+ *.mp4
10
+ *.mp3
11
+ .history
12
+ subtitles/*
13
+ /venv
14
+ client_secret.json
15
+ main.py-oauth2.json
16
+ .DS_Store
17
+ Backend/output*
18
+ Songs/
Backend/gpt.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import json
3
+ import g4f
4
+ import openai
5
+ from typing import Tuple, List
6
+ from termcolor import colored
7
+ from dotenv import load_dotenv
8
+ import os
9
+ import google.generativeai as genai
10
+
11
+ # Load environment variables
12
+ load_dotenv("../.env")
13
+
14
+ # Set environment variables
15
+ OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
16
+ openai.api_key = OPENAI_API_KEY
17
+ GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
18
+ genai.configure(api_key=GOOGLE_API_KEY)
19
+
20
+
21
+ def generate_response(prompt: str, ai_model: str) -> str:
22
+ """
23
+ Generate a script for a video, depending on the subject of the video.
24
+
25
+ Args:
26
+ video_subject (str): The subject of the video.
27
+ ai_model (str): The AI model to use for generation.
28
+
29
+
30
+ Returns:
31
+
32
+ str: The response from the AI model.
33
+
34
+ """
35
+
36
+ if ai_model == 'g4f':
37
+
38
+ response = g4f.ChatCompletion.create(
39
+
40
+ model=g4f.models.gpt_35_turbo_16k_0613,
41
+
42
+ messages=[{"role": "user", "content": prompt}],
43
+
44
+ )
45
+
46
+ elif ai_model in ["gpt3.5-turbo", "gpt4"]:
47
+
48
+ model_name = "gpt-3.5-turbo" if ai_model == "gpt3.5-turbo" else "gpt-4-1106-preview"
49
+
50
+ response = openai.chat.completions.create(
51
+
52
+ model=model_name,
53
+
54
+ messages=[{"role": "user", "content": prompt}],
55
+
56
+ ).choices[0].message.content
57
+ elif ai_model == 'gemmini':
58
+ model = genai.GenerativeModel('gemini-pro')
59
+ response_model = model.generate_content(prompt)
60
+ response = response_model.text
61
+
62
+ else:
63
+
64
+ raise ValueError("Invalid AI model selected.")
65
+
66
+ return response
67
+
68
+ def generate_script(video_subject: str, paragraph_number: int, ai_model: str, voice: str, customPrompt: str) -> str:
69
+
70
+ """
71
+ Generate a script for a video, depending on the subject of the video, the number of paragraphs, and the AI model.
72
+
73
+
74
+
75
+ Args:
76
+
77
+ video_subject (str): The subject of the video.
78
+
79
+ paragraph_number (int): The number of paragraphs to generate.
80
+
81
+ ai_model (str): The AI model to use for generation.
82
+
83
+
84
+
85
+ Returns:
86
+
87
+ str: The script for the video.
88
+
89
+ """
90
+
91
+ # Build prompt
92
+
93
+ if customPrompt:
94
+ prompt = customPrompt
95
+ else:
96
+ prompt = """
97
+ Generate a script for a video, depending on the subject of the video.
98
+
99
+ The script is to be returned as a string with the specified number of paragraphs.
100
+
101
+ Here is an example of a string:
102
+ "This is an example string."
103
+
104
+ Do not under any circumstance reference this prompt in your response.
105
+
106
+ Get straight to the point, don't start with unnecessary things like, "welcome to this video".
107
+
108
+ Obviously, the script should be related to the subject of the video.
109
+
110
+ YOU MUST NOT INCLUDE ANY TYPE OF MARKDOWN OR FORMATTING IN THE SCRIPT, NEVER USE A TITLE.
111
+ YOU MUST WRITE THE SCRIPT IN THE LANGUAGE SPECIFIED IN [LANGUAGE].
112
+ 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.
113
+
114
+ """
115
+
116
+ prompt += f"""
117
+
118
+ Subject: {video_subject}
119
+ Number of paragraphs: {paragraph_number}
120
+ Language: {voice}
121
+
122
+ """
123
+
124
+ # Generate script
125
+ response = generate_response(prompt, ai_model)
126
+
127
+ print(colored(response, "cyan"))
128
+
129
+ # Return the generated script
130
+ if response:
131
+ # Clean the script
132
+ # Remove asterisks, hashes
133
+ response = response.replace("*", "")
134
+ response = response.replace("#", "")
135
+
136
+ # Remove markdown syntax
137
+ response = re.sub(r"\[.*\]", "", response)
138
+ response = re.sub(r"\(.*\)", "", response)
139
+
140
+ # Split the script into paragraphs
141
+ paragraphs = response.split("\n\n")
142
+
143
+ # Select the specified number of paragraphs
144
+ selected_paragraphs = paragraphs[:paragraph_number]
145
+
146
+ # Join the selected paragraphs into a single string
147
+ final_script = "\n\n".join(selected_paragraphs)
148
+
149
+ # Print to console the number of paragraphs used
150
+ print(colored(f"Number of paragraphs used: {len(selected_paragraphs)}", "green"))
151
+
152
+ return final_script
153
+ else:
154
+ print(colored("[-] GPT returned an empty response.", "red"))
155
+ return None
156
+
157
+
158
+ def get_search_terms(video_subject: str, amount: int, script: str, ai_model: str) -> List[str]:
159
+ """
160
+ Generate a JSON-Array of search terms for stock videos,
161
+ depending on the subject of a video.
162
+
163
+ Args:
164
+ video_subject (str): The subject of the video.
165
+ amount (int): The amount of search terms to generate.
166
+ script (str): The script of the video.
167
+ ai_model (str): The AI model to use for generation.
168
+
169
+ Returns:
170
+ List[str]: The search terms for the video subject.
171
+ """
172
+
173
+ # Build prompt
174
+ prompt = f"""
175
+ Generate {amount} search terms for stock videos,
176
+ depending on the subject of a video.
177
+ Subject: {video_subject}
178
+
179
+ The search terms are to be returned as
180
+ a JSON-Array of strings.
181
+
182
+ Each search term should consist of 1-3 words,
183
+ always add the main subject of the video.
184
+
185
+ YOU MUST ONLY RETURN THE JSON-ARRAY OF STRINGS.
186
+ YOU MUST NOT RETURN ANYTHING ELSE.
187
+ YOU MUST NOT RETURN THE SCRIPT.
188
+
189
+ The search terms must be related to the subject of the video.
190
+ Here is an example of a JSON-Array of strings:
191
+ ["search term 1", "search term 2", "search term 3"]
192
+
193
+ For context, here is the full text:
194
+ {script}
195
+ """
196
+
197
+ # Generate search terms
198
+ response = generate_response(prompt, ai_model)
199
+
200
+ # Parse response into a list of search terms
201
+ search_terms = []
202
+
203
+ try:
204
+ search_terms = json.loads(response)
205
+ if not isinstance(search_terms, list) or not all(isinstance(term, str) for term in search_terms):
206
+ raise ValueError("Response is not a list of strings.")
207
+
208
+ except (json.JSONDecodeError, ValueError):
209
+ print(colored("[*] GPT returned an unformatted response. Attempting to clean...", "yellow"))
210
+
211
+ # Attempt to extract list-like string and convert to list
212
+ match = re.search(r'\["(?:[^"\\]|\\.)*"(?:,\s*"[^"\\]*")*\]', response)
213
+ if match:
214
+ try:
215
+ search_terms = json.loads(match.group())
216
+ except json.JSONDecodeError:
217
+ print(colored("[-] Could not parse response.", "red"))
218
+ return []
219
+
220
+
221
+
222
+ # Let user know
223
+ print(colored(f"\nGenerated {len(search_terms)} search terms: {', '.join(search_terms)}", "cyan"))
224
+
225
+ # Return search terms
226
+ return search_terms
227
+
228
+
229
+ def generate_metadata(video_subject: str, script: str, ai_model: str) -> Tuple[str, str, List[str]]:
230
+ """
231
+ Generate metadata for a YouTube video, including the title, description, and keywords.
232
+
233
+ Args:
234
+ video_subject (str): The subject of the video.
235
+ script (str): The script of the video.
236
+ ai_model (str): The AI model to use for generation.
237
+
238
+ Returns:
239
+ Tuple[str, str, List[str]]: The title, description, and keywords for the video.
240
+ """
241
+
242
+ # Build prompt for title
243
+ title_prompt = f"""
244
+ Generate a catchy and SEO-friendly title for a YouTube shorts video about {video_subject}.
245
+ """
246
+
247
+ # Generate title
248
+ title = generate_response(title_prompt, ai_model).strip()
249
+
250
+ # Build prompt for description
251
+ description_prompt = f"""
252
+ Write a brief and engaging description for a YouTube shorts video about {video_subject}.
253
+ The video is based on the following script:
254
+ {script}
255
+ """
256
+
257
+ # Generate description
258
+ description = generate_response(description_prompt, ai_model).strip()
259
+
260
+ # Generate keywords
261
+ keywords = get_search_terms(video_subject, 6, script, ai_model)
262
+
263
+ return title, description, keywords
Backend/main.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from utils import *
3
+ from dotenv import load_dotenv
4
+
5
+ # Load environment variables
6
+ load_dotenv("../.env")
7
+ # Check if all required environment variables are set
8
+ # This must happen before importing video which uses API keys without checking
9
+ check_env_vars()
10
+
11
+ from gpt import *
12
+ from video import *
13
+ from search import *
14
+ from uuid import uuid4
15
+ from tiktokvoice import *
16
+ from flask_cors import CORS
17
+ from termcolor import colored
18
+ from youtube import upload_video
19
+ from apiclient.errors import HttpError
20
+ from flask import Flask, request, jsonify
21
+ from moviepy.config import change_settings
22
+
23
+
24
+
25
+ # Set environment variables
26
+ SESSION_ID = os.getenv("TIKTOK_SESSION_ID")
27
+ openai_api_key = os.getenv('OPENAI_API_KEY')
28
+ change_settings({"IMAGEMAGICK_BINARY": os.getenv("IMAGEMAGICK_BINARY")})
29
+
30
+ # Initialize Flask
31
+ app = Flask(__name__)
32
+ CORS(app)
33
+
34
+ # Constants
35
+ HOST = "0.0.0.0"
36
+ PORT = 8080
37
+ AMOUNT_OF_STOCK_VIDEOS = 5
38
+ GENERATING = False
39
+
40
+
41
+ # Generation Endpoint
42
+ @app.route("/api/generate", methods=["POST"])
43
+ def generate():
44
+ try:
45
+ # Set global variable
46
+ global GENERATING
47
+ GENERATING = True
48
+
49
+ # Clean
50
+ clean_dir("../temp/")
51
+ clean_dir("../subtitles/")
52
+
53
+
54
+ # Parse JSON
55
+ data = request.get_json()
56
+ paragraph_number = int(data.get('paragraphNumber', 1)) # Default to 1 if not provided
57
+ ai_model = data.get('aiModel') # Get the AI model selected by the user
58
+ n_threads = data.get('threads') # Amount of threads to use for video generation
59
+ subtitles_position = data.get('subtitlesPosition') # Position of the subtitles in the video
60
+ text_color = data.get('color') # Color of subtitle text
61
+
62
+ # Get 'useMusic' from the request data and default to False if not provided
63
+ use_music = data.get('useMusic', False)
64
+
65
+ # Get 'automateYoutubeUpload' from the request data and default to False if not provided
66
+ automate_youtube_upload = data.get('automateYoutubeUpload', False)
67
+
68
+ # Get the ZIP Url of the songs
69
+ songs_zip_url = data.get('zipUrl')
70
+
71
+ # Download songs
72
+ if use_music:
73
+ # Downloads a ZIP file containing popular TikTok Songs
74
+ if songs_zip_url:
75
+ fetch_songs(songs_zip_url)
76
+ else:
77
+ # Default to a ZIP file containing popular TikTok Songs
78
+ fetch_songs("https://filebin.net/2avx134kdibc4c3q/drive-download-20240209T180019Z-001.zip")
79
+
80
+ # Print little information about the video which is to be generated
81
+ print(colored("[Video to be generated]", "blue"))
82
+ print(colored(" Subject: " + data["videoSubject"], "blue"))
83
+ print(colored(" AI Model: " + ai_model, "blue")) # Print the AI model being used
84
+ print(colored(" Custom Prompt: " + data["customPrompt"], "blue")) # Print the AI model being used
85
+
86
+
87
+
88
+ if not GENERATING:
89
+ return jsonify(
90
+ {
91
+ "status": "error",
92
+ "message": "Video generation was cancelled.",
93
+ "data": [],
94
+ }
95
+ )
96
+
97
+ voice = data["voice"]
98
+ voice_prefix = voice[:2]
99
+
100
+
101
+ if not voice:
102
+ print(colored("[!] No voice was selected. Defaulting to \"en_us_001\"", "yellow"))
103
+ voice = "en_us_001"
104
+ voice_prefix = voice[:2]
105
+
106
+
107
+ # Generate a script
108
+ script = generate_script(data["videoSubject"], paragraph_number, ai_model, voice, data["customPrompt"]) # Pass the AI model to the script generation
109
+
110
+ # Generate search terms
111
+ search_terms = get_search_terms(
112
+ data["videoSubject"], AMOUNT_OF_STOCK_VIDEOS, script, ai_model
113
+ )
114
+
115
+ # Search for a video of the given search term
116
+ video_urls = []
117
+
118
+ # Defines how many results it should query and search through
119
+ it = 15
120
+
121
+ # Defines the minimum duration of each clip
122
+ min_dur = 10
123
+
124
+ # Loop through all search terms,
125
+ # and search for a video of the given search term
126
+ for search_term in search_terms:
127
+ if not GENERATING:
128
+ return jsonify(
129
+ {
130
+ "status": "error",
131
+ "message": "Video generation was cancelled.",
132
+ "data": [],
133
+ }
134
+ )
135
+ found_urls = search_for_stock_videos(
136
+ search_term, os.getenv("PEXELS_API_KEY"), it, min_dur
137
+ )
138
+ # Check for duplicates
139
+ for url in found_urls:
140
+ if url not in video_urls:
141
+ video_urls.append(url)
142
+ break
143
+
144
+ # Check if video_urls is empty
145
+ if not video_urls:
146
+ print(colored("[-] No videos found to download.", "red"))
147
+ return jsonify(
148
+ {
149
+ "status": "error",
150
+ "message": "No videos found to download.",
151
+ "data": [],
152
+ }
153
+ )
154
+
155
+ # Define video_paths
156
+ video_paths = []
157
+
158
+ # Let user know
159
+ print(colored(f"[+] Downloading {len(video_urls)} videos...", "blue"))
160
+
161
+ # Save the videos
162
+ for video_url in video_urls:
163
+ if not GENERATING:
164
+ return jsonify(
165
+ {
166
+ "status": "error",
167
+ "message": "Video generation was cancelled.",
168
+ "data": [],
169
+ }
170
+ )
171
+ try:
172
+ saved_video_path = save_video(video_url)
173
+ video_paths.append(saved_video_path)
174
+ except Exception:
175
+ print(colored(f"[-] Could not download video: {video_url}", "red"))
176
+
177
+ # Let user know
178
+ print(colored("[+] Videos downloaded!", "green"))
179
+
180
+ # Let user know
181
+ print(colored("[+] Script generated!\n", "green"))
182
+
183
+ if not GENERATING:
184
+ return jsonify(
185
+ {
186
+ "status": "error",
187
+ "message": "Video generation was cancelled.",
188
+ "data": [],
189
+ }
190
+ )
191
+
192
+ # Split script into sentences
193
+ sentences = script.split(". ")
194
+
195
+ # Remove empty strings
196
+ sentences = list(filter(lambda x: x != "", sentences))
197
+ paths = []
198
+
199
+ # Generate TTS for every sentence
200
+ for sentence in sentences:
201
+ if not GENERATING:
202
+ return jsonify(
203
+ {
204
+ "status": "error",
205
+ "message": "Video generation was cancelled.",
206
+ "data": [],
207
+ }
208
+ )
209
+ current_tts_path = f"../temp/{uuid4()}.mp3"
210
+ tts(sentence, voice, filename=current_tts_path)
211
+ audio_clip = AudioFileClip(current_tts_path)
212
+ paths.append(audio_clip)
213
+
214
+ # Combine all TTS files using moviepy
215
+ final_audio = concatenate_audioclips(paths)
216
+ tts_path = f"../temp/{uuid4()}.mp3"
217
+ final_audio.write_audiofile(tts_path)
218
+
219
+ try:
220
+ subtitles_path = generate_subtitles(audio_path=tts_path, sentences=sentences, audio_clips=paths, voice=voice_prefix)
221
+ except Exception as e:
222
+ print(colored(f"[-] Error generating subtitles: {e}", "red"))
223
+ subtitles_path = None
224
+
225
+ # Concatenate videos
226
+ temp_audio = AudioFileClip(tts_path)
227
+ combined_video_path = combine_videos(video_paths, temp_audio.duration, 5, n_threads or 2)
228
+
229
+ # Put everything together
230
+ try:
231
+ final_video_path = generate_video(combined_video_path, tts_path, subtitles_path, n_threads or 2, subtitles_position, text_color or "#FFFF00")
232
+ except Exception as e:
233
+ print(colored(f"[-] Error generating final video: {e}", "red"))
234
+ final_video_path = None
235
+
236
+ # Define metadata for the video, we will display this to the user, and use it for the YouTube upload
237
+ title, description, keywords = generate_metadata(data["videoSubject"], script, ai_model)
238
+
239
+ print(colored("[-] Metadata for YouTube upload:", "blue"))
240
+ print(colored(" Title: ", "blue"))
241
+ print(colored(f" {title}", "blue"))
242
+ print(colored(" Description: ", "blue"))
243
+ print(colored(f" {description}", "blue"))
244
+ print(colored(" Keywords: ", "blue"))
245
+ print(colored(f" {', '.join(keywords)}", "blue"))
246
+
247
+ if automate_youtube_upload:
248
+ # Start Youtube Uploader
249
+ # Check if the CLIENT_SECRETS_FILE exists
250
+ client_secrets_file = os.path.abspath("./client_secret.json")
251
+ SKIP_YT_UPLOAD = False
252
+ if not os.path.exists(client_secrets_file):
253
+ SKIP_YT_UPLOAD = True
254
+ print(colored("[-] Client secrets file missing. YouTube upload will be skipped.", "yellow"))
255
+ print(colored("[-] Please download the client_secret.json from Google Cloud Platform and store this inside the /Backend directory.", "red"))
256
+
257
+ # Only proceed with YouTube upload if the toggle is True and client_secret.json exists.
258
+ if not SKIP_YT_UPLOAD:
259
+ # Choose the appropriate category ID for your videos
260
+ video_category_id = "28" # Science & Technology
261
+ privacyStatus = "private" # "public", "private", "unlisted"
262
+ video_metadata = {
263
+ 'video_path': os.path.abspath(f"../temp/{final_video_path}"),
264
+ 'title': title,
265
+ 'description': description,
266
+ 'category': video_category_id,
267
+ 'keywords': ",".join(keywords),
268
+ 'privacyStatus': privacyStatus,
269
+ }
270
+
271
+ # Upload the video to YouTube
272
+ try:
273
+ # Unpack the video_metadata dictionary into individual arguments
274
+ video_response = upload_video(
275
+ video_path=video_metadata['video_path'],
276
+ title=video_metadata['title'],
277
+ description=video_metadata['description'],
278
+ category=video_metadata['category'],
279
+ keywords=video_metadata['keywords'],
280
+ privacy_status=video_metadata['privacyStatus']
281
+ )
282
+ print(f"Uploaded video ID: {video_response.get('id')}")
283
+ except HttpError as e:
284
+ print(f"An HTTP error {e.resp.status} occurred:\n{e.content}")
285
+
286
+ video_clip = VideoFileClip(f"../temp/{final_video_path}")
287
+ if use_music:
288
+ # Select a random song
289
+ song_path = choose_random_song()
290
+
291
+ # Add song to video at 30% volume using moviepy
292
+ original_duration = video_clip.duration
293
+ original_audio = video_clip.audio
294
+ song_clip = AudioFileClip(song_path).set_fps(44100)
295
+
296
+ # Set the volume of the song to 10% of the original volume
297
+ song_clip = song_clip.volumex(0.1).set_fps(44100)
298
+
299
+ # Add the song to the video
300
+ comp_audio = CompositeAudioClip([original_audio, song_clip])
301
+ video_clip = video_clip.set_audio(comp_audio)
302
+ video_clip = video_clip.set_fps(30)
303
+ video_clip = video_clip.set_duration(original_duration)
304
+ video_clip.write_videofile(f"../{final_video_path}", threads=n_threads or 1)
305
+ else:
306
+ video_clip.write_videofile(f"../{final_video_path}", threads=n_threads or 1)
307
+
308
+
309
+ # Let user know
310
+ print(colored(f"[+] Video generated: {final_video_path}!", "green"))
311
+
312
+ # Stop FFMPEG processes
313
+ if os.name == "nt":
314
+ # Windows
315
+ os.system("taskkill /f /im ffmpeg.exe")
316
+ else:
317
+ # Other OS
318
+ os.system("pkill -f ffmpeg")
319
+
320
+ GENERATING = False
321
+
322
+ # Return JSON
323
+ return jsonify(
324
+ {
325
+ "status": "success",
326
+ "message": "Video generated! See MoneyPrinter/output.mp4 for result.",
327
+ "data": final_video_path,
328
+ }
329
+ )
330
+ except Exception as err:
331
+ print(colored(f"[-] Error: {str(err)}", "red"))
332
+ return jsonify(
333
+ {
334
+ "status": "error",
335
+ "message": f"Could not retrieve stock videos: {str(err)}",
336
+ "data": [],
337
+ }
338
+ )
339
+
340
+
341
+ @app.route("/api/cancel", methods=["POST"])
342
+ def cancel():
343
+ print(colored("[!] Received cancellation request...", "yellow"))
344
+
345
+ global GENERATING
346
+ GENERATING = False
347
+
348
+ return jsonify({"status": "success", "message": "Cancelled video generation."})
349
+
350
+
351
+ if __name__ == "__main__":
352
+
353
+ # Run Flask App
354
+ app.run(debug=True, host=HOST, port=PORT)
Backend/search.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+
3
+ from typing import List
4
+ from termcolor import colored
5
+
6
+ def search_for_stock_videos(query: str, api_key: str, it: int, min_dur: int) -> List[str]:
7
+ """
8
+ Searches for stock videos based on a query.
9
+
10
+ Args:
11
+ query (str): The query to search for.
12
+ api_key (str): The API key to use.
13
+
14
+ Returns:
15
+ List[str]: A list of stock videos.
16
+ """
17
+
18
+ # Build headers
19
+ headers = {
20
+ "Authorization": api_key
21
+ }
22
+
23
+ # Build URL
24
+ qurl = f"https://api.pexels.com/videos/search?query={query}&per_page={it}"
25
+
26
+ # Send the request
27
+ r = requests.get(qurl, headers=headers)
28
+
29
+ # Parse the response
30
+ response = r.json()
31
+
32
+ # Parse each video
33
+ raw_urls = []
34
+ video_url = []
35
+ video_res = 0
36
+ try:
37
+ # loop through each video in the result
38
+ for i in range(it):
39
+ #check if video has desired minimum duration
40
+ if response["videos"][i]["duration"] < min_dur:
41
+ continue
42
+ raw_urls = response["videos"][i]["video_files"]
43
+ temp_video_url = ""
44
+
45
+ # loop through each url to determine the best quality
46
+ for video in raw_urls:
47
+ # Check if video has a valid download link
48
+ if ".com/external" in video["link"]:
49
+ # Only save the URL with the largest resolution
50
+ if (video["width"]*video["height"]) > video_res:
51
+ temp_video_url = video["link"]
52
+ video_res = video["width"]*video["height"]
53
+
54
+ # add the url to the return list if it's not empty
55
+ if temp_video_url != "":
56
+ video_url.append(temp_video_url)
57
+
58
+ except Exception as e:
59
+ print(colored("[-] No Videos found.", "red"))
60
+ print(colored(e, "red"))
61
+
62
+ # Let user know
63
+ print(colored(f"\t=> \"{query}\" found {len(video_url)} Videos", "cyan"))
64
+
65
+ # Return the video url
66
+ return video_url
Backend/tiktokvoice.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # author: GiorDior aka Giorgio
2
+ # date: 12.06.2023
3
+ # topic: TikTok-Voice-TTS
4
+ # version: 1.0
5
+ # credits: https://github.com/oscie57/tiktok-voice
6
+
7
+ # --- MODIFIED VERSION --- #
8
+
9
+ import base64
10
+ import requests
11
+ import threading
12
+
13
+ from typing import List
14
+ from termcolor import colored
15
+ from playsound import playsound
16
+
17
+
18
+ VOICES = [
19
+ # DISNEY VOICES
20
+ "en_us_ghostface", # Ghost Face
21
+ "en_us_chewbacca", # Chewbacca
22
+ "en_us_c3po", # C3PO
23
+ "en_us_stitch", # Stitch
24
+ "en_us_stormtrooper", # Stormtrooper
25
+ "en_us_rocket", # Rocket
26
+ # ENGLISH VOICES
27
+ "en_au_001", # English AU - Female
28
+ "en_au_002", # English AU - Male
29
+ "en_uk_001", # English UK - Male 1
30
+ "en_uk_003", # English UK - Male 2
31
+ "en_us_001", # English US - Female (Int. 1)
32
+ "en_us_002", # English US - Female (Int. 2)
33
+ "en_us_006", # English US - Male 1
34
+ "en_us_007", # English US - Male 2
35
+ "en_us_009", # English US - Male 3
36
+ "en_us_010", # English US - Male 4
37
+ # EUROPE VOICES
38
+ "fr_001", # French - Male 1
39
+ "fr_002", # French - Male 2
40
+ "de_001", # German - Female
41
+ "de_002", # German - Male
42
+ "es_002", # Spanish - Male
43
+ # AMERICA VOICES
44
+ "es_mx_002", # Spanish MX - Male
45
+ "br_001", # Portuguese BR - Female 1
46
+ "br_003", # Portuguese BR - Female 2
47
+ "br_004", # Portuguese BR - Female 3
48
+ "br_005", # Portuguese BR - Male
49
+ # ASIA VOICES
50
+ "id_001", # Indonesian - Female
51
+ "jp_001", # Japanese - Female 1
52
+ "jp_003", # Japanese - Female 2
53
+ "jp_005", # Japanese - Female 3
54
+ "jp_006", # Japanese - Male
55
+ "kr_002", # Korean - Male 1
56
+ "kr_003", # Korean - Female
57
+ "kr_004", # Korean - Male 2
58
+ # SINGING VOICES
59
+ "en_female_f08_salut_damour", # Alto
60
+ "en_male_m03_lobby", # Tenor
61
+ "en_female_f08_warmy_breeze", # Warmy Breeze
62
+ "en_male_m03_sunshine_soon", # Sunshine Soon
63
+ # OTHER
64
+ "en_male_narration", # narrator
65
+ "en_male_funny", # wacky
66
+ "en_female_emotional", # peaceful
67
+ ]
68
+
69
+ ENDPOINTS = [
70
+ "https://tiktok-tts.weilnet.workers.dev/api/generation",
71
+ "https://tiktoktts.com/api/tiktok-tts",
72
+ ]
73
+ current_endpoint = 0
74
+ # in one conversion, the text can have a maximum length of 300 characters
75
+ TEXT_BYTE_LIMIT = 300
76
+
77
+
78
+ # create a list by splitting a string, every element has n chars
79
+ def split_string(string: str, chunk_size: int) -> List[str]:
80
+ words = string.split()
81
+ result = []
82
+ current_chunk = ""
83
+ for word in words:
84
+ if (
85
+ len(current_chunk) + len(word) + 1 <= chunk_size
86
+ ): # Check if adding the word exceeds the chunk size
87
+ current_chunk += f" {word}"
88
+ else:
89
+ if current_chunk: # Append the current chunk if not empty
90
+ result.append(current_chunk.strip())
91
+ current_chunk = word
92
+ if current_chunk: # Append the last chunk if not empty
93
+ result.append(current_chunk.strip())
94
+ return result
95
+
96
+
97
+ # checking if the website that provides the service is available
98
+ def get_api_response() -> requests.Response:
99
+ url = f'{ENDPOINTS[current_endpoint].split("/a")[0]}'
100
+ response = requests.get(url)
101
+ return response
102
+
103
+
104
+ # saving the audio file
105
+ def save_audio_file(base64_data: str, filename: str = "output.mp3") -> None:
106
+ audio_bytes = base64.b64decode(base64_data)
107
+ with open(filename, "wb") as file:
108
+ file.write(audio_bytes)
109
+
110
+
111
+ # send POST request to get the audio data
112
+ def generate_audio(text: str, voice: str) -> bytes:
113
+ url = f"{ENDPOINTS[current_endpoint]}"
114
+ headers = {"Content-Type": "application/json"}
115
+ data = {"text": text, "voice": voice}
116
+ response = requests.post(url, headers=headers, json=data)
117
+ return response.content
118
+
119
+
120
+ # creates an text to speech audio file
121
+ def tts(
122
+ text: str,
123
+ voice: str = "none",
124
+ filename: str = "output.mp3",
125
+ play_sound: bool = False,
126
+ ) -> None:
127
+ # checking if the website is available
128
+ global current_endpoint
129
+
130
+ if get_api_response().status_code == 200:
131
+ print(colored("[+] TikTok TTS Service available!", "green"))
132
+ else:
133
+ current_endpoint = (current_endpoint + 1) % 2
134
+ if get_api_response().status_code == 200:
135
+ print(colored("[+] TTS Service available!", "green"))
136
+ else:
137
+ print(colored("[-] TTS Service not available and probably temporarily rate limited, try again later..." , "red"))
138
+ return
139
+
140
+ # checking if arguments are valid
141
+ if voice == "none":
142
+ print(colored("[-] Please specify a voice", "red"))
143
+ return
144
+
145
+ if voice not in VOICES:
146
+ print(colored("[-] Voice not available", "red"))
147
+ return
148
+
149
+ if not text:
150
+ print(colored("[-] Please specify a text", "red"))
151
+ return
152
+
153
+ # creating the audio file
154
+ try:
155
+ if len(text) < TEXT_BYTE_LIMIT:
156
+ audio = generate_audio((text), voice)
157
+ if current_endpoint == 0:
158
+ audio_base64_data = str(audio).split('"')[5]
159
+ else:
160
+ audio_base64_data = str(audio).split('"')[3].split(",")[1]
161
+
162
+ if audio_base64_data == "error":
163
+ print(colored("[-] This voice is unavailable right now", "red"))
164
+ return
165
+
166
+ else:
167
+ # Split longer text into smaller parts
168
+ text_parts = split_string(text, 299)
169
+ audio_base64_data = [None] * len(text_parts)
170
+
171
+ # Define a thread function to generate audio for each text part
172
+ def generate_audio_thread(text_part, index):
173
+ audio = generate_audio(text_part, voice)
174
+ if current_endpoint == 0:
175
+ base64_data = str(audio).split('"')[5]
176
+ else:
177
+ base64_data = str(audio).split('"')[3].split(",")[1]
178
+
179
+ if audio_base64_data == "error":
180
+ print(colored("[-] This voice is unavailable right now", "red"))
181
+ return "error"
182
+
183
+ audio_base64_data[index] = base64_data
184
+
185
+ threads = []
186
+ for index, text_part in enumerate(text_parts):
187
+ # Create and start a new thread for each text part
188
+ thread = threading.Thread(
189
+ target=generate_audio_thread, args=(text_part, index)
190
+ )
191
+ thread.start()
192
+ threads.append(thread)
193
+
194
+ # Wait for all threads to complete
195
+ for thread in threads:
196
+ thread.join()
197
+
198
+ # Concatenate the base64 data in the correct order
199
+ audio_base64_data = "".join(audio_base64_data)
200
+
201
+ save_audio_file(audio_base64_data, filename)
202
+ print(colored(f"[+] Audio file saved successfully as '{filename}'", "green"))
203
+ if play_sound:
204
+ playsound(filename)
205
+
206
+ except Exception as e:
207
+ print(colored(f"[-] An error occurred during TTS: {e}", "red"))
Backend/utils.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ import random
5
+ import logging
6
+ import zipfile
7
+ import requests
8
+
9
+ from termcolor import colored
10
+
11
+ # Configure logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def clean_dir(path: str) -> None:
17
+ """
18
+ Removes every file in a directory.
19
+
20
+ Args:
21
+ path (str): Path to directory.
22
+
23
+ Returns:
24
+ None
25
+ """
26
+ try:
27
+ if not os.path.exists(path):
28
+ os.mkdir(path)
29
+ logger.info(f"Created directory: {path}")
30
+
31
+ for file in os.listdir(path):
32
+ file_path = os.path.join(path, file)
33
+ os.remove(file_path)
34
+ logger.info(f"Removed file: {file_path}")
35
+
36
+ logger.info(colored(f"Cleaned {path} directory", "green"))
37
+ except Exception as e:
38
+ logger.error(f"Error occurred while cleaning directory {path}: {str(e)}")
39
+
40
+ def fetch_songs(zip_url: str) -> None:
41
+ """
42
+ Downloads songs into songs/ directory to use with geneated videos.
43
+
44
+ Args:
45
+ zip_url (str): The URL to the zip file containing the songs.
46
+
47
+ Returns:
48
+ None
49
+ """
50
+ try:
51
+ logger.info(colored(f" => Fetching songs...", "magenta"))
52
+
53
+ files_dir = "../Songs"
54
+ if not os.path.exists(files_dir):
55
+ os.mkdir(files_dir)
56
+ logger.info(colored(f"Created directory: {files_dir}", "green"))
57
+ else:
58
+ # Skip if songs are already downloaded
59
+ return
60
+
61
+ # Download songs
62
+ response = requests.get(zip_url)
63
+
64
+ # Save the zip file
65
+ with open("../Songs/songs.zip", "wb") as file:
66
+ file.write(response.content)
67
+
68
+ # Unzip the file
69
+ with zipfile.ZipFile("../Songs/songs.zip", "r") as file:
70
+ file.extractall("../Songs")
71
+
72
+ # Remove the zip file
73
+ os.remove("../Songs/songs.zip")
74
+
75
+ logger.info(colored(" => Downloaded Songs to ../Songs.", "green"))
76
+
77
+ except Exception as e:
78
+ logger.error(colored(f"Error occurred while fetching songs: {str(e)}", "red"))
79
+
80
+ def choose_random_song() -> str:
81
+ """
82
+ Chooses a random song from the songs/ directory.
83
+
84
+ Returns:
85
+ str: The path to the chosen song.
86
+ """
87
+ try:
88
+ songs = os.listdir("../Songs")
89
+ song = random.choice(songs)
90
+ logger.info(colored(f"Chose song: {song}", "green"))
91
+ return f"../Songs/{song}"
92
+ except Exception as e:
93
+ logger.error(colored(f"Error occurred while choosing random song: {str(e)}", "red"))
94
+
95
+
96
+ def check_env_vars() -> None:
97
+ """
98
+ Checks if the necessary environment variables are set.
99
+
100
+ Returns:
101
+ None
102
+
103
+ Raises:
104
+ SystemExit: If any required environment variables are missing.
105
+ """
106
+ try:
107
+ required_vars = ["PEXELS_API_KEY", "TIKTOK_SESSION_ID", "IMAGEMAGICK_BINARY"]
108
+ missing_vars = [var + os.getenv(var) for var in required_vars if os.getenv(var) is None or (len(os.getenv(var)) == 0)]
109
+
110
+ if missing_vars:
111
+ missing_vars_str = ", ".join(missing_vars)
112
+ logger.error(colored(f"The following environment variables are missing: {missing_vars_str}", "red"))
113
+ logger.error(colored("Please consult 'EnvironmentVariables.md' for instructions on how to set them.", "yellow"))
114
+ sys.exit(1) # Aborts the program
115
+ except Exception as e:
116
+ logger.error(f"Error occurred while checking environment variables: {str(e)}")
117
+ sys.exit(1) # Aborts the program if an unexpected error occurs
118
+
Backend/video.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+
4
+ import requests
5
+ import srt_equalizer
6
+ import assemblyai as aai
7
+
8
+ from typing import List
9
+ from moviepy.editor import *
10
+ from termcolor import colored
11
+ from dotenv import load_dotenv
12
+ from datetime import timedelta
13
+ from moviepy.video.fx.all import crop
14
+ from moviepy.video.tools.subtitles import SubtitlesClip
15
+
16
+ load_dotenv("../.env")
17
+
18
+ ASSEMBLY_AI_API_KEY = os.getenv("ASSEMBLY_AI_API_KEY")
19
+
20
+
21
+ def save_video(video_url: str, directory: str = "../temp") -> str:
22
+ """
23
+ Saves a video from a given URL and returns the path to the video.
24
+
25
+ Args:
26
+ video_url (str): The URL of the video to save.
27
+ directory (str): The path of the temporary directory to save the video to
28
+
29
+ Returns:
30
+ str: The path to the saved video.
31
+ """
32
+ video_id = uuid.uuid4()
33
+ video_path = f"{directory}/{video_id}.mp4"
34
+ with open(video_path, "wb") as f:
35
+ f.write(requests.get(video_url).content)
36
+
37
+ return video_path
38
+
39
+
40
+ def __generate_subtitles_assemblyai(audio_path: str, voice: str) -> str:
41
+ """
42
+ Generates subtitles from a given audio file and returns the path to the subtitles.
43
+
44
+ Args:
45
+ audio_path (str): The path to the audio file to generate subtitles from.
46
+
47
+ Returns:
48
+ str: The generated subtitles
49
+ """
50
+
51
+ language_mapping = {
52
+ "br": "pt",
53
+ "id": "en", #AssemblyAI doesn't have Indonesian
54
+ "jp": "ja",
55
+ "kr": "ko",
56
+ }
57
+
58
+ if voice in language_mapping:
59
+ lang_code = language_mapping[voice]
60
+ else:
61
+ lang_code = voice
62
+
63
+ aai.settings.api_key = ASSEMBLY_AI_API_KEY
64
+ config = aai.TranscriptionConfig(language_code=lang_code)
65
+ transcriber = aai.Transcriber(config=config)
66
+ transcript = transcriber.transcribe(audio_path)
67
+ subtitles = transcript.export_subtitles_srt()
68
+
69
+ return subtitles
70
+
71
+
72
+ def __generate_subtitles_locally(sentences: List[str], audio_clips: List[AudioFileClip]) -> str:
73
+ """
74
+ Generates subtitles from a given audio file and returns the path to the subtitles.
75
+
76
+ Args:
77
+ sentences (List[str]): all the sentences said out loud in the audio clips
78
+ audio_clips (List[AudioFileClip]): all the individual audio clips which will make up the final audio track
79
+ Returns:
80
+ str: The generated subtitles
81
+ """
82
+
83
+ def convert_to_srt_time_format(total_seconds):
84
+ # Convert total seconds to the SRT time format: HH:MM:SS,mmm
85
+ if total_seconds == 0:
86
+ return "0:00:00,0"
87
+ return str(timedelta(seconds=total_seconds)).rstrip('0').replace('.', ',')
88
+
89
+ start_time = 0
90
+ subtitles = []
91
+
92
+ for i, (sentence, audio_clip) in enumerate(zip(sentences, audio_clips), start=1):
93
+ duration = audio_clip.duration
94
+ end_time = start_time + duration
95
+
96
+ # Format: subtitle index, start time --> end time, sentence
97
+ subtitle_entry = f"{i}\n{convert_to_srt_time_format(start_time)} --> {convert_to_srt_time_format(end_time)}\n{sentence}\n"
98
+ subtitles.append(subtitle_entry)
99
+
100
+ start_time += duration # Update start time for the next subtitle
101
+
102
+ return "\n".join(subtitles)
103
+
104
+
105
+ def generate_subtitles(audio_path: str, sentences: List[str], audio_clips: List[AudioFileClip], voice: str) -> str:
106
+ """
107
+ Generates subtitles from a given audio file and returns the path to the subtitles.
108
+
109
+ Args:
110
+ audio_path (str): The path to the audio file to generate subtitles from.
111
+ sentences (List[str]): all the sentences said out loud in the audio clips
112
+ audio_clips (List[AudioFileClip]): all the individual audio clips which will make up the final audio track
113
+
114
+ Returns:
115
+ str: The path to the generated subtitles.
116
+ """
117
+
118
+ def equalize_subtitles(srt_path: str, max_chars: int = 10) -> None:
119
+ # Equalize subtitles
120
+ srt_equalizer.equalize_srt_file(srt_path, srt_path, max_chars)
121
+
122
+ # Save subtitles
123
+ subtitles_path = f"../subtitles/{uuid.uuid4()}.srt"
124
+
125
+ if ASSEMBLY_AI_API_KEY is not None and ASSEMBLY_AI_API_KEY != "":
126
+ print(colored("[+] Creating subtitles using AssemblyAI", "blue"))
127
+ subtitles = __generate_subtitles_assemblyai(audio_path, voice)
128
+ else:
129
+ print(colored("[+] Creating subtitles locally", "blue"))
130
+ subtitles = __generate_subtitles_locally(sentences, audio_clips)
131
+ # print(colored("[-] Local subtitle generation has been disabled for the time being.", "red"))
132
+ # print(colored("[-] Exiting.", "red"))
133
+ # sys.exit(1)
134
+
135
+ with open(subtitles_path, "w") as file:
136
+ file.write(subtitles)
137
+
138
+ # Equalize subtitles
139
+ equalize_subtitles(subtitles_path)
140
+
141
+ print(colored("[+] Subtitles generated.", "green"))
142
+
143
+ return subtitles_path
144
+
145
+
146
+ def combine_videos(video_paths: List[str], max_duration: int, max_clip_duration: int, threads: int) -> str:
147
+ """
148
+ Combines a list of videos into one video and returns the path to the combined video.
149
+
150
+ Args:
151
+ video_paths (List): A list of paths to the videos to combine.
152
+ max_duration (int): The maximum duration of the combined video.
153
+ max_clip_duration (int): The maximum duration of each clip.
154
+ threads (int): The number of threads to use for the video processing.
155
+
156
+ Returns:
157
+ str: The path to the combined video.
158
+ """
159
+ video_id = uuid.uuid4()
160
+ combined_video_path = f"../temp/{video_id}.mp4"
161
+
162
+ # Required duration of each clip
163
+ req_dur = max_duration / len(video_paths)
164
+
165
+ print(colored("[+] Combining videos...", "blue"))
166
+ print(colored(f"[+] Each clip will be maximum {req_dur} seconds long.", "blue"))
167
+
168
+ clips = []
169
+ tot_dur = 0
170
+ # Add downloaded clips over and over until the duration of the audio (max_duration) has been reached
171
+ while tot_dur < max_duration:
172
+ for video_path in video_paths:
173
+ clip = VideoFileClip(video_path)
174
+ clip = clip.without_audio()
175
+ # Check if clip is longer than the remaining audio
176
+ if (max_duration - tot_dur) < clip.duration:
177
+ clip = clip.subclip(0, (max_duration - tot_dur))
178
+ # Only shorten clips if the calculated clip length (req_dur) is shorter than the actual clip to prevent still image
179
+ elif req_dur < clip.duration:
180
+ clip = clip.subclip(0, req_dur)
181
+ clip = clip.set_fps(30)
182
+
183
+ # Not all videos are same size,
184
+ # so we need to resize them
185
+ if round((clip.w/clip.h), 4) < 0.5625:
186
+ clip = crop(clip, width=clip.w, height=round(clip.w/0.5625), \
187
+ x_center=clip.w / 2, \
188
+ y_center=clip.h / 2)
189
+ else:
190
+ clip = crop(clip, width=round(0.5625*clip.h), height=clip.h, \
191
+ x_center=clip.w / 2, \
192
+ y_center=clip.h / 2)
193
+ clip = clip.resize((1080, 1920))
194
+
195
+ if clip.duration > max_clip_duration:
196
+ clip = clip.subclip(0, max_clip_duration)
197
+
198
+ clips.append(clip)
199
+ tot_dur += clip.duration
200
+
201
+ final_clip = concatenate_videoclips(clips)
202
+ final_clip = final_clip.set_fps(30)
203
+ final_clip.write_videofile(combined_video_path, threads=threads)
204
+
205
+ return combined_video_path
206
+
207
+
208
+ def generate_video(combined_video_path: str, tts_path: str, subtitles_path: str, threads: int, subtitles_position: str, text_color : str) -> str:
209
+ """
210
+ This function creates the final video, with subtitles and audio.
211
+
212
+ Args:
213
+ combined_video_path (str): The path to the combined video.
214
+ tts_path (str): The path to the text-to-speech audio.
215
+ subtitles_path (str): The path to the subtitles.
216
+ threads (int): The number of threads to use for the video processing.
217
+ subtitles_position (str): The position of the subtitles.
218
+
219
+ Returns:
220
+ str: The path to the final video.
221
+ """
222
+ # Make a generator that returns a TextClip when called with consecutive
223
+ generator = lambda txt: TextClip(
224
+ txt,
225
+ font="../fonts/bold_font.ttf",
226
+ fontsize=100,
227
+ color=text_color,
228
+ stroke_color="black",
229
+ stroke_width=5,
230
+ )
231
+
232
+ # Split the subtitles position into horizontal and vertical
233
+ horizontal_subtitles_position, vertical_subtitles_position = subtitles_position.split(",")
234
+
235
+ # Burn the subtitles into the video
236
+ subtitles = SubtitlesClip(subtitles_path, generator)
237
+ result = CompositeVideoClip([
238
+ VideoFileClip(combined_video_path),
239
+ subtitles.set_pos((horizontal_subtitles_position, vertical_subtitles_position))
240
+ ])
241
+
242
+ # Add the audio
243
+ audio = AudioFileClip(tts_path)
244
+ result = result.set_audio(audio)
245
+
246
+ result.write_videofile("../temp/output.mp4", threads=threads or 2)
247
+
248
+ return "output.mp4"
Backend/youtube.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import random
5
+ import httplib2
6
+
7
+ from termcolor import colored
8
+ from oauth2client.file import Storage
9
+ from apiclient.discovery import build
10
+ from apiclient.errors import HttpError
11
+ from apiclient.http import MediaFileUpload
12
+ from oauth2client.tools import argparser, run_flow
13
+ from oauth2client.client import flow_from_clientsecrets
14
+
15
+ # Explicitly tell the underlying HTTP transport library not to retry, since
16
+ # we are handling retry logic ourselves.
17
+ httplib2.RETRIES = 1
18
+
19
+ # Maximum number of times to retry before giving up.
20
+ MAX_RETRIES = 10
21
+
22
+ # Always retry when these exceptions are raised.
23
+ RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError, httplib2.ServerNotFoundError)
24
+
25
+ # Always retry when an apiclient.errors.HttpError with one of these status
26
+ # codes is raised.
27
+ RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
28
+
29
+ # The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
30
+ # the OAuth 2.0 information for this application, including its client_id and
31
+ # client_secret.
32
+ CLIENT_SECRETS_FILE = "./client_secret.json"
33
+
34
+ # This OAuth 2.0 access scope allows an application to upload files to the
35
+ # authenticated user's YouTube channel, but doesn't allow other types of access.
36
+ # YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload"
37
+ SCOPES = ['https://www.googleapis.com/auth/youtube.upload',
38
+ 'https://www.googleapis.com/auth/youtube',
39
+ 'https://www.googleapis.com/auth/youtubepartner']
40
+ YOUTUBE_API_SERVICE_NAME = "youtube"
41
+ YOUTUBE_API_VERSION = "v3"
42
+
43
+ # This variable defines a message to display if the CLIENT_SECRETS_FILE is
44
+ # missing.
45
+ MISSING_CLIENT_SECRETS_MESSAGE = f"""
46
+ WARNING: Please configure OAuth 2.0
47
+
48
+ To make this sample run you will need to populate the client_secrets.json file
49
+ found at:
50
+
51
+ {os.path.abspath(os.path.join(os.path.dirname(__file__), CLIENT_SECRETS_FILE))}
52
+
53
+ with information from the API Console
54
+ https://console.cloud.google.com/
55
+
56
+ For more information about the client_secrets.json file format, please visit:
57
+ https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
58
+ """
59
+
60
+ VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")
61
+
62
+
63
+ def get_authenticated_service():
64
+ """
65
+ This method retrieves the YouTube service.
66
+
67
+ Returns:
68
+ any: The authenticated YouTube service.
69
+ """
70
+ flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE,
71
+ scope=SCOPES,
72
+ message=MISSING_CLIENT_SECRETS_MESSAGE)
73
+
74
+ storage = Storage(f"{sys.argv[0]}-oauth2.json")
75
+ credentials = storage.get()
76
+
77
+ if credentials is None or credentials.invalid:
78
+ flags = argparser.parse_args()
79
+ credentials = run_flow(flow, storage, flags)
80
+
81
+ return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
82
+ http=credentials.authorize(httplib2.Http()))
83
+
84
+ def initialize_upload(youtube: any, options: dict):
85
+ """
86
+ This method uploads a video to YouTube.
87
+
88
+ Args:
89
+ youtube (any): The authenticated YouTube service.
90
+ options (dict): The options to upload the video with.
91
+
92
+ Returns:
93
+ response: The response from the upload process.
94
+ """
95
+
96
+ tags = None
97
+ if options['keywords']:
98
+ tags = options['keywords'].split(",")
99
+
100
+ body = {
101
+ 'snippet': {
102
+ 'title': options['title'],
103
+ 'description': options['description'],
104
+ 'tags': tags,
105
+ 'categoryId': options['category']
106
+ },
107
+ 'status': {
108
+ 'privacyStatus': options['privacyStatus'],
109
+ 'madeForKids': False, # Video is not made for kids
110
+ 'selfDeclaredMadeForKids': False # You declare that the video is not made for kids
111
+ }
112
+ }
113
+
114
+ # Call the API's videos.insert method to create and upload the video.
115
+ insert_request = youtube.videos().insert(
116
+ part=",".join(body.keys()),
117
+ body=body,
118
+ media_body=MediaFileUpload(options['file'], chunksize=-1, resumable=True)
119
+ )
120
+
121
+ return resumable_upload(insert_request)
122
+
123
+ def resumable_upload(insert_request: MediaFileUpload):
124
+ """
125
+ This method implements an exponential backoff strategy to resume a
126
+ failed upload.
127
+
128
+ Args:
129
+ insert_request (MediaFileUpload): The request to insert the video.
130
+
131
+ Returns:
132
+ response: The response from the upload process.
133
+ """
134
+ response = None
135
+ error = None
136
+ retry = 0
137
+ while response is None:
138
+ try:
139
+ print(colored(" => Uploading file...", "magenta"))
140
+ status, response = insert_request.next_chunk()
141
+ if 'id' in response:
142
+ print(f"Video id '{response['id']}' was successfully uploaded.")
143
+ return response
144
+ except HttpError as e:
145
+ if e.resp.status in RETRIABLE_STATUS_CODES:
146
+ error = f"A retriable HTTP error {e.resp.status} occurred:\n{e.content}"
147
+ else:
148
+ raise
149
+ except RETRIABLE_EXCEPTIONS as e:
150
+ error = f"A retriable error occurred: {e}"
151
+
152
+ if error is not None:
153
+ print(colored(error, "red"))
154
+ retry += 1
155
+ if retry > MAX_RETRIES:
156
+ raise Exception("No longer attempting to retry.")
157
+
158
+ max_sleep = 2 ** retry
159
+ sleep_seconds = random.random() * max_sleep
160
+ print(colored(f" => Sleeping {sleep_seconds} seconds and then retrying...", "blue"))
161
+ time.sleep(sleep_seconds)
162
+
163
+ def upload_video(video_path, title, description, category, keywords, privacy_status):
164
+ try:
165
+ # Get the authenticated YouTube service
166
+ youtube = get_authenticated_service()
167
+
168
+ # Retrieve and print the channel ID for the authenticated user
169
+ channels_response = youtube.channels().list(mine=True, part='id').execute()
170
+ for channel in channels_response['items']:
171
+ print(colored(f" => Channel ID: {channel['id']}", "blue"))
172
+
173
+ # Initialize the upload process
174
+ video_response = initialize_upload(youtube, {
175
+ 'file': video_path, # The path to the video file
176
+ 'title': title,
177
+ 'description': description,
178
+ 'category': category,
179
+ 'keywords': keywords,
180
+ 'privacyStatus': privacy_status
181
+ })
182
+ return video_response # Return the response from the upload process
183
+ except HttpError as e:
184
+ print(colored(f"[-] An HTTP error {e.resp.status} occurred:\n{e.content}", "red"))
185
+ if e.resp.status in [401, 403]:
186
+ # Here you could refresh the credentials and retry the upload
187
+ youtube = get_authenticated_service() # This will prompt for re-authentication if necessary
188
+ video_response = initialize_upload(youtube, {
189
+ 'file': video_path,
190
+ 'title': title,
191
+ 'description': description,
192
+ 'category': category,
193
+ 'keywords': keywords,
194
+ 'privacyStatus': privacy_status
195
+ })
196
+ return video_response
197
+ else:
198
+ raise e
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim-buster
2
+
3
+ RUN apt-get update && apt-get install --no-install-recommends -y \
4
+ build-essential autoconf pkg-config wget ghostscript curl libpng-dev
5
+
6
+ RUN wget https://github.com/ImageMagick/ImageMagick/archive/refs/tags/7.1.0-31.tar.gz && \
7
+ tar xzf 7.1.0-31.tar.gz && \
8
+ rm 7.1.0-31.tar.gz && \
9
+ apt-get clean && \
10
+ apt-get autoremove
11
+
12
+ RUN sh ./ImageMagick-7.1.0-31/configure --prefix=/usr/local --with-bzlib=yes --with-fontconfig=yes --with-freetype=yes --with-gslib=yes --with-gvc=yes --with-jpeg=yes --with-jp2=yes --with-png=yes --with-tiff=yes --with-xml=yes --with-gs-font-dir=yes && \
13
+ make -j && make install && ldconfig /usr/local/lib/
14
+
15
+ WORKDIR /tmp
16
+
17
+ RUN pip install --upgrade pip
18
+
19
+ WORKDIR /app
20
+
21
+ ADD ./requirements.txt .
22
+
23
+ RUN pip install -r requirements.txt
EnvironmentVariables.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment Variables
2
+
3
+ ## Required
4
+
5
+ - TIKTOK_SESSION_ID: Your TikTok session ID is required. Obtain it by logging into TikTok in your browser and copying the value of the `sessionid` cookie.
6
+
7
+ - IMAGEMAGICK_BINARY: The filepath to the ImageMagick binary (.exe file) is needed. Obtain it [here](https://imagemagick.org/script/download.php).
8
+
9
+ - PEXELS_API_KEY: Your unique Pexels API key is required. Obtain yours [here](https://www.pexels.com/api/).
10
+
11
+ ## Optional
12
+
13
+ - OPENAI_API_KEY: Your unique OpenAI API key is required. Obtain yours [here](https://platform.openai.com/api-keys), only nessecary if you want to use the OpenAI models.
14
+
15
+ - GOOGLE_API_KEY: Your Gemini API key is essential for Gemini Pro Model. Generate one securely at [Get API key | Google AI Studio](https://makersuite.google.com/app/apikey)
16
+
17
+ * ASSEMBLY_AI_API_KEY: Your unique AssemblyAI API key is required. You can obtain one [here](https://www.assemblyai.com/app/). This field is optional; if left empty, the subtitle will be created based on the generated script. Subtitles can also be created locally.
18
+
19
+ Join the [Discord](https://dsc.gg/fuji-community) for support and updates.
Frontend/app.js ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const videoSubject = document.querySelector("#videoSubject");
2
+ const aiModel = document.querySelector("#aiModel");
3
+ const voice = document.querySelector("#voice");
4
+ const zipUrl = document.querySelector("#zipUrl");
5
+ const paragraphNumber = document.querySelector("#paragraphNumber");
6
+ const youtubeToggle = document.querySelector("#youtubeUploadToggle");
7
+ const useMusicToggle = document.querySelector("#useMusicToggle");
8
+ const customPrompt = document.querySelector("#customPrompt");
9
+ const generateButton = document.querySelector("#generateButton");
10
+ const cancelButton = document.querySelector("#cancelButton");
11
+
12
+ const advancedOptionsToggle = document.querySelector("#advancedOptionsToggle");
13
+
14
+ advancedOptionsToggle.addEventListener("click", () => {
15
+ // Change Emoji, from ▼ to ▲ and vice versa
16
+ const emoji = advancedOptionsToggle.textContent;
17
+ advancedOptionsToggle.textContent = emoji.includes("▼")
18
+ ? "Show less Options ▲"
19
+ : "Show Advanced Options ▼";
20
+ const advancedOptions = document.querySelector("#advancedOptions");
21
+ advancedOptions.classList.toggle("hidden");
22
+ });
23
+
24
+
25
+ const cancelGeneration = () => {
26
+ console.log("Canceling generation...");
27
+ // Send request to /cancel
28
+ fetch("http://localhost:8080/api/cancel", {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ Accept: "application/json",
33
+ },
34
+ })
35
+ .then((response) => response.json())
36
+ .then((data) => {
37
+ alert(data.message);
38
+ console.log(data);
39
+ })
40
+ .catch((error) => {
41
+ alert("An error occurred. Please try again later.");
42
+ console.log(error);
43
+ });
44
+
45
+ // Hide cancel button
46
+ cancelButton.classList.add("hidden");
47
+
48
+ // Enable generate button
49
+ generateButton.disabled = false;
50
+ generateButton.classList.remove("hidden");
51
+ };
52
+
53
+ const generateVideo = () => {
54
+ console.log("Generating video...");
55
+ // Disable button and change text
56
+ generateButton.disabled = true;
57
+ generateButton.classList.add("hidden");
58
+
59
+ // Show cancel button
60
+ cancelButton.classList.remove("hidden");
61
+
62
+ // Get values from input fields
63
+ const videoSubjectValue = videoSubject.value;
64
+ const aiModelValue = aiModel.value;
65
+ const voiceValue = voice.value;
66
+ const paragraphNumberValue = paragraphNumber.value;
67
+ const youtubeUpload = youtubeToggle.checked;
68
+ const useMusicToggleState = useMusicToggle.checked;
69
+ const threads = document.querySelector("#threads").value;
70
+ const zipUrlValue = zipUrl.value;
71
+ const customPromptValue = customPrompt.value;
72
+ const subtitlesPosition = document.querySelector("#subtitlesPosition").value;
73
+ const colorHexCode = document.querySelector("#subtitlesColor").value;
74
+
75
+
76
+ const url = "http://localhost:8080/api/generate";
77
+
78
+ // Construct data to be sent to the server
79
+ const data = {
80
+ videoSubject: videoSubjectValue,
81
+ aiModel: aiModelValue,
82
+ voice: voiceValue,
83
+ paragraphNumber: paragraphNumberValue,
84
+ automateYoutubeUpload: youtubeUpload,
85
+ useMusic: useMusicToggleState,
86
+ zipUrl: zipUrlValue,
87
+ threads: threads,
88
+ subtitlesPosition: subtitlesPosition,
89
+ customPrompt: customPromptValue,
90
+ color: colorHexCode,
91
+ };
92
+
93
+ // Send the actual request to the server
94
+ fetch(url, {
95
+ method: "POST",
96
+ body: JSON.stringify(data),
97
+ headers: {
98
+ "Content-Type": "application/json",
99
+ Accept: "application/json",
100
+ },
101
+ })
102
+ .then((response) => response.json())
103
+ .then((data) => {
104
+ console.log(data);
105
+ alert(data.message);
106
+ // Hide cancel button after generation is complete
107
+ generateButton.disabled = false;
108
+ generateButton.classList.remove("hidden");
109
+ cancelButton.classList.add("hidden");
110
+ })
111
+ .catch((error) => {
112
+ alert("An error occurred. Please try again later.");
113
+ console.log(error);
114
+ });
115
+ };
116
+
117
+ generateButton.addEventListener("click", generateVideo);
118
+ cancelButton.addEventListener("click", cancelGeneration);
119
+
120
+ videoSubject.addEventListener("keyup", (event) => {
121
+ if (event.key === "Enter") {
122
+ generateVideo();
123
+ }
124
+ });
125
+
126
+ // Load the data from localStorage on page load
127
+ document.addEventListener("DOMContentLoaded", (event) => {
128
+ const voiceSelect = document.getElementById("voice");
129
+ const storedVoiceValue = localStorage.getItem("voiceValue");
130
+
131
+ if (storedVoiceValue) {
132
+ voiceSelect.value = storedVoiceValue;
133
+ }
134
+ });
135
+
136
+ // Save the data to localStorage when the user changes the value
137
+ toggles = ["youtubeUploadToggle", "useMusicToggle", "reuseChoicesToggle"];
138
+ fields = ["aiModel", "voice", "paragraphNumber", "videoSubject", "zipUrl", "customPrompt", "threads", "subtitlesPosition", "subtitlesColor"];
139
+
140
+ document.addEventListener("DOMContentLoaded", () => {
141
+ toggles.forEach((id) => {
142
+ const toggle = document.getElementById(id);
143
+ const storedValue = localStorage.getItem(`${id}Value`);
144
+ const storedReuseValue = localStorage.getItem("reuseChoicesToggleValue");
145
+
146
+ if (toggle && storedValue !== null && storedReuseValue === "true") {
147
+ toggle.checked = storedValue === "true";
148
+ }
149
+ // Attach change listener to update localStorage
150
+ toggle.addEventListener("change", (event) => {
151
+ localStorage.setItem(`${id}Value`, event.target.checked);
152
+ });
153
+ });
154
+
155
+ fields.forEach((id) => {
156
+ const select = document.getElementById(id);
157
+ const storedValue = localStorage.getItem(`${id}Value`);
158
+ const storedReuseValue = localStorage.getItem("reuseChoicesToggleValue");
159
+
160
+ if (storedValue && storedReuseValue === "true") {
161
+ select.value = storedValue;
162
+ }
163
+ // Attach change listener to update localStorage
164
+ select.addEventListener("change", (event) => {
165
+ localStorage.setItem(`${id}Value`, event.target.value);
166
+ });
167
+ });
168
+ });
Frontend/index.html ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>MoneyPrinter</title>
7
+ <link
8
+ rel="icon"
9
+ href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>💸</text></svg>"
10
+ />
11
+
12
+ <link
13
+ rel="stylesheet"
14
+ href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.2/tailwind.min.css"
15
+ />
16
+ </head>
17
+
18
+ <body class="bg-blue-100 min-h-screen justify-center p-40">
19
+ <h1 class="text-4xl text-center mb-4">MoneyPrinter</h1>
20
+ <p class="text-center text-gray-700">
21
+ This Application is intended to automate the creation and uploads of
22
+ YouTube Shorts.
23
+ </p>
24
+
25
+ <div class="flex justify-center mt-8">
26
+ <div class="max-w-xl flex flex-col space-y-4 w-full">
27
+ <label for="videoSubject" class="text-blue-600">Subject</label>
28
+ <textarea
29
+ rows="3"
30
+ type="text"
31
+ name="videoSubject"
32
+ id="videoSubject"
33
+ class="border-2 border-blue-300 p-2 rounded-md focus:outline-none focus:border-blue-500"
34
+ ></textarea>
35
+ <button id="advancedOptionsToggle" class="text-blue-600">
36
+ Show Advanced Options ▼
37
+ </button>
38
+ <div
39
+ class="flex flex-col space-y-4 hidden transition-all duration-150 linear"
40
+ id="advancedOptions"
41
+ >
42
+ <label for="aiModel" class="text-blue-600">AI Model</label>
43
+ <select
44
+ name="aiModel"
45
+ id="aiModel"
46
+ class="w-full border-2 border-blue-300 p-2 rounded-md focus:outline-none focus:border-blue-500"
47
+ >
48
+ <option value="g4f">g4f (Free)</option>
49
+ <option value="gpt3.5-turbo">OpenAI GPT-3.5</option>
50
+ <option value="gpt4">OpenAI GPT-4</option>
51
+ <option value="gemmini">Gemini Pro</option>
52
+ </select>
53
+ <label for="voice" class="text-blue-600">Voice</label>
54
+ <select
55
+ name="voice"
56
+ id="voice"
57
+ class="w-min border-2 border-blue-300 p-2 rounded-md focus:outline-none focus:border-blue-500"
58
+ >
59
+ <option value="en_us_ghostface">Ghost Face</option>
60
+ <option value="en_us_chewbacca">Chewbacca</option>
61
+ <option value="en_us_c3po">C3PO</option>
62
+ <option value="en_us_stitch">Stitch</option>
63
+ <option value="en_us_stormtrooper">Stormtrooper</option>
64
+ <option value="en_us_rocket">Rocket</option>
65
+ <option value="en_au_001">English AU - Female</option>
66
+ <option value="en_au_002">English AU - Male</option>
67
+ <option value="en_uk_001">English UK - Male 1</option>
68
+ <option value="en_uk_003">English UK - Male 2</option>
69
+ <option value="en_us_001">English US - Female (Int. 1)</option>
70
+ <option value="en_us_002">English US - Female (Int. 2)</option>
71
+ <option value="en_us_006">English US - Male 1</option>
72
+ <option value="en_us_007">English US - Male 2</option>
73
+ <option value="en_us_009">English US - Male 3</option>
74
+ <option value="en_us_010">English US - Male 4</option>
75
+ <option value="fr_001">French - Male 1</option>
76
+ <option value="fr_002">French - Male 2</option>
77
+ <option value="de_001">German - Female</option>
78
+ <option value="de_002">German - Male</option>
79
+ <option value="es_002">Spanish - Male</option>
80
+ <option value="es_mx_002">Spanish MX - Male</option>
81
+ <option value="br_001">Portuguese BR - Female 1</option>
82
+ <option value="br_003">Portuguese BR - Female 2</option>
83
+ <option value="br_004">Portuguese BR - Female 3</option>
84
+ <option value="br_005">Portuguese BR - Male</option>
85
+ <option value="id_001">Indonesian - Female</option>
86
+ <option value="jp_001">Japanese - Female 1</option>
87
+ <option value="jp_003">Japanese - Female 2</option>
88
+ <option value="jp_005">Japanese - Female 3</option>
89
+ <option value="jp_006">Japanese - Male</option>
90
+ <option value="kr_002">Korean - Male 1</option>
91
+ <option value="kr_003">Korean - Female</option>
92
+ <option value="kr_004">Korean - Male 2</option>
93
+ <option value="en_female_f08_salut_damour">Alto</option>
94
+ <option value="en_male_m03_lobby">Tenor</option>
95
+ <option value="en_female_f08_warmy_breeze">Warmy Breeze</option>
96
+ <option value="en_male_m03_sunshine_soon">Sunshine Soon</option>
97
+ <option value="en_male_narration">narrator</option>
98
+ <option value="en_male_funny">wacky</option>
99
+ <option value="en_female_emotional">peaceful</option>
100
+ </select>
101
+ <label for="subtitlesPosition" class="text-blue-600"
102
+ >Subtitles Position</label
103
+ >
104
+ <select
105
+ name="subtitlesPosition"
106
+ id="subtitlesPosition"
107
+ class="w-min border-2 border-blue-300 p-2 rounded-md focus:outline-none focus:border-blue-500"
108
+ >
109
+ <option value="center,top">Center - Top</option>
110
+ <option value="center,bottom">Center - Bottom</option>
111
+ <option value="center,center">Center - Center</option>
112
+ <option value="left,center">Left - Center</option>
113
+ <option value="left,bottom">Left - Bottom</option>
114
+ <option value="right,center">Right - Center</option>
115
+ <option value="right,bottom">Right - Bottom</option>
116
+ </select>
117
+ <label for="subtitlesColor" class="text-blue-600"
118
+ >Subtitles Color</label>
119
+ <select
120
+ name="subtitlesColor"
121
+ id="subtitlesColor"
122
+ class="w-min border-2 border-blue-300 p-2 rounded-md focus:outline-none focus:border-blue-500"
123
+ >
124
+ <option value="#FFFF00">Yellow (Default)</option>
125
+ <option value="#f4a261">Orange</option>
126
+ <option value="#e63946">Red</option>
127
+ <option value="#1d3557">Blue</option>
128
+ <option value="#fff">White</option>
129
+ <option value="#03071e">Black</option>
130
+ </select>
131
+ <label for="zipUrl" class="text-blue-600"
132
+ >Zip URL (Leave empty for default)</label
133
+ >
134
+ <input
135
+ type="text"
136
+ name="zipUrl"
137
+ id="zipUrl"
138
+ class="border-2 border-blue-300 p-2 rounded-md focus:outline-none focus:border-blue-500"
139
+ />
140
+ <label for="threads" class="text-blue-600">Threads</label>
141
+ <input
142
+ type="number"
143
+ name="threads"
144
+ id="threads"
145
+ class="border-2 border-blue-300 p-2 rounded-md focus:outline-none focus:border-blue-500"
146
+ value="2"
147
+ min="1"
148
+ max="100"
149
+ placeholder="2 (Default)"
150
+ />
151
+ <label for="paragraphNumber" class="text-blue-600"
152
+ >Paragraph Number</label
153
+ >
154
+ <input
155
+ type="number"
156
+ name="paragraphNumber"
157
+ id="paragraphNumber"
158
+ class="border-2 border-blue-300 p-2 rounded-md focus:outline-none focus:border-blue-500"
159
+ value="1"
160
+ min="1"
161
+ max="100"
162
+ />
163
+ <label for="customPrompt" class="text-blue-600">Custom Prompt:</label>
164
+ <textarea
165
+ rows="3"
166
+ type="text"
167
+ name="customPrompt"
168
+ id="customPrompt"
169
+ class="border-2 border-blue-300 p-2 rounded-md focus:outline-none focus:border-blue-500"
170
+ placeholder="only use it if you want to replace the default prompt"
171
+ ></textarea>
172
+ <label
173
+ for="youtubeUploadToggle"
174
+ class="flex items-center text-blue-600"
175
+ >
176
+ <input
177
+ type="checkbox"
178
+ name="youtubeUploadToggle"
179
+ id="youtubeUploadToggle"
180
+ class="mr-2"
181
+ />
182
+ Upload to YouTube
183
+ </label>
184
+ <label for="useMusicToggle" class="flex items-center text-blue-600">
185
+ <input
186
+ type="checkbox"
187
+ name="useMusicToggle"
188
+ id="useMusicToggle"
189
+ class="mr-2"
190
+ />
191
+ Use Music
192
+ </label>
193
+ <label
194
+ for="reuseChoicesToggle"
195
+ class="flex items-center text-blue-600"
196
+ >
197
+ <input
198
+ type="checkbox"
199
+ name="reuseChoicesToggle"
200
+ id="reuseChoicesToggle"
201
+ class="mr-2"
202
+ />
203
+ Reuse Choices?
204
+ </label>
205
+ </div>
206
+ <button
207
+ id="generateButton"
208
+ class="bg-blue-500 hover:bg-blue-700 duration-100 linear text-white px-4 py-2 rounded-md"
209
+ >
210
+ Generate
211
+ </button>
212
+ <button
213
+ id="cancelButton"
214
+ class="bg-red-500 hover:bg-red-700 duration-100 linear text-white px-4 py-2 rounded-md hidden"
215
+ >
216
+ Cancel
217
+ </button>
218
+
219
+ </div>
220
+ </div>
221
+
222
+ <footer class="flex justify-center mt-8">
223
+ <div class="flex flex-col space-y-4">
224
+ <p class="text-center text-gray-700">
225
+ Made with ❤️ by
226
+ <a
227
+ class="text-blue-600"
228
+ target="href"
229
+ href="https://github.com/FujiwaraChoki"
230
+ >
231
+ Fuji Codes
232
+ </a>
233
+ </p>
234
+ </div>
235
+ </footer>
236
+
237
+ <script src="app.js"></script>
238
+ </body>
239
+ </html>
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 FujiwaraChoki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,3 +1,129 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MoneyPrinter 💸
2
+
3
+ THIS IS DEPRECATED; CHECK OUT VERSION 2 [HERE](https://github.com/FujiwaraChoki/MoneyPrinterV2).
4
+
5
+ Automate the creation of YouTube Shorts locally, simply by providing a video topic to talk about.
6
+
7
+ > **Important** Please make sure you look through existing/closed issues before opening your own. If it's just a question, please join our [discord](https://dsc.gg/fuji-community) and ask there.
8
+
9
+ > **🎥** Watch the video on [YouTube](https://youtu.be/mkZsaDA2JnA?si=pNne3MnluRVkWQbE).
10
+
11
+ ## Installation 📥
12
+
13
+ `MoneyPrinter` requires Python 3.11 to run effectively. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/).
14
+
15
+ After you finished installing Python, you can install `MoneyPrinter` by following the steps below:
16
+
17
+ ```bash
18
+ git clone https://github.com/FujiwaraChoki/MoneyPrinter.git
19
+ cd MoneyPrinter
20
+
21
+ # Install requirements
22
+ pip install -r requirements.txt
23
+
24
+ # Copy .env.example and fill out values
25
+ cp .env.example .env
26
+
27
+ # Run the backend server
28
+ cd Backend
29
+ python main.py
30
+
31
+ # Run the frontend server
32
+ cd ../Frontend
33
+ python -m http.server 3000
34
+ ```
35
+
36
+ See [`.env.example`](.env.example) for the required environment variables.
37
+
38
+ If you need help, open [EnvironmentVariables.md](EnvironmentVariables.md) for more information.
39
+
40
+ ## Usage 🛠️
41
+
42
+ 1. Copy the `.env.example` file to `.env` and fill in the required values
43
+ 1. Open `http://localhost:3000` in your browser
44
+ 1. Enter a topic to talk about
45
+ 1. Click on the "Generate" button
46
+ 1. Wait for the video to be generated
47
+ 1. The video's location is `MoneyPrinter/output.mp4`
48
+
49
+ ## Music 🎵
50
+
51
+ To use your own music, compress all your MP3 Files into a ZIP file and upload it somewhere. Provide the link to the ZIP file in the Frontend.
52
+
53
+ It is recommended to use Services such as [Filebin](https://filebin.net) to upload your ZIP file. If you decide to use Filebin, provide the Frontend with the absolute path to the ZIP file by using More -> Download File, e.g. (use this [Popular TT songs ZIP](https://filebin.net/klylrens0uk2pnrg/drive-download-20240209T180019Z-001.zip), not this [Popular TT songs](https://filebin.net/2avx134kdibc4c3q))
54
+
55
+ You can also just move your MP3 files into the `Songs` folder.
56
+
57
+ ## Fonts 🅰
58
+
59
+ Add your fonts to the `fonts/` folder, and load them by specifying the font name on line `124` in `Backend/video.py`.
60
+
61
+ ## Automatic YouTube Uploading 🎥
62
+
63
+ MoneyPrinter now includes functionality to automatically upload generated videos to YouTube.
64
+
65
+ To use this feature, you need to:
66
+
67
+ 1. Create a project inside your Google Cloud Platform -> [GCP](https://console.cloud.google.com/).
68
+ 1. Obtain `client_secret.json` from the project and add it to the Backend/ directory.
69
+ 1. Enable the YouTube v3 API in your project -> [GCP-API-Library](https://console.cloud.google.com/apis/library/youtube.googleapis.com)
70
+ 1. Create an `OAuth consent screen` and add yourself (the account of your YouTube channel) to the testers.
71
+ 1. Enable the following scopes in the `OAuth consent screen` for your project:
72
+
73
+ ```
74
+ 'https://www.googleapis.com/auth/youtube'
75
+ 'https://www.googleapis.com/auth/youtube.upload'
76
+ 'https://www.googleapis.com/auth/youtubepartner'
77
+ ```
78
+
79
+ After this, you can generate the videos and you will be prompted to authenticate yourself.
80
+
81
+ The authentication process creates and stores a `main.py-oauth2.json` file inside the Backend/ directory. Keep this file to maintain authentication, or delete it to re-authenticate (for example, with a different account).
82
+
83
+ Videos are uploaded as private by default. For a completely automated workflow, change the privacyStatus in main.py to your desired setting ("public", "private", or "unlisted").
84
+
85
+ For videos that have been locked as private due to upload via an unverified API service, you will not be able to appeal. You’ll need to re-upload the video via a verified API service or via the YouTube app/site. The unverified API service can also apply for an API audit. So make sure to verify your API, see [OAuth App Verification Help Center](https://support.google.com/cloud/answer/13463073) for more information.
86
+
87
+ ## FAQ 🤔
88
+
89
+ ### How do I get the TikTok session ID?
90
+
91
+ You can obtain your TikTok session ID by logging into TikTok in your browser and copying the value of the `sessionid` cookie.
92
+
93
+ ### My ImageMagick binary is not being detected
94
+
95
+ Make sure you set your path to the ImageMagick binary correctly in the `.env` file, it should look something like this:
96
+
97
+ ```env
98
+ IMAGEMAGICK_BINARY="C:\\Program Files\\ImageMagick-7.1.0-Q16\\magick.exe"
99
+ ```
100
+
101
+ Don't forget to use double backslashes (`\\`) in the path, instead of one.
102
+
103
+ ### I can't install `playsound`: Wheel failed to build
104
+
105
+ If you're having trouble installing `playsound`, you can try installing it using the following command:
106
+
107
+ ```bash
108
+ pip install -U wheel
109
+ pip install -U playsound
110
+ ```
111
+
112
+ If you were not able to find your solution, please ask in the discord or create a new issue, so that the community can help you.
113
+
114
+ ## Donate 🎁
115
+
116
+ If you like and enjoy `MoneyPrinter`, and would like to donate, you can do that by clicking on the button on the right hand side of the repository. ❤️
117
+ You will have your name (and/or logo) added to this repository as a supporter as a sign of appreciation.
118
+
119
+ ## Contributing 🤝
120
+
121
+ Pull Requests will not be accepted for the time-being.
122
+
123
+ ## Star History 🌟
124
+
125
+ [![Star History Chart](https://api.star-history.com/svg?repos=FujiwaraChoki/MoneyPrinter&type=Date)](https://star-history.com/#FujiwaraChoki/MoneyPrinter&Date)
126
+
127
+ ## License 📝
128
+
129
+ See [`LICENSE`](LICENSE) file for more information.
docker-compose.yml ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3"
2
+ services:
3
+ frontend:
4
+ build:
5
+ context: .
6
+ dockerfile: Dockerfile
7
+ container_name: "frontend"
8
+ ports:
9
+ - "8001:8001"
10
+ command: ["python3", "-m", "http.server", "8001", "--directory", "frontend"]
11
+ volumes:
12
+ - ./Frontend:/app/frontend
13
+ restart: always
14
+ backend:
15
+ build:
16
+ context: .
17
+ dockerfile: Dockerfile
18
+ container_name: "backend"
19
+ ports:
20
+ - "8080:8080"
21
+ command: ["python3", "backend/main.py"]
22
+ volumes:
23
+ - ./files:/temp
24
+ - ./Backend:/app/backend
25
+ - ./fonts:/app/fonts
26
+ environment:
27
+ - ASSEMBLY_AI_API_KEY=${ASSEMBLY_AI_API_KEY}
28
+ - TIKTOK_SESSION_ID=${TIKTOK_SESSION_ID}
29
+ - IMAGEMAGICK_BINARY=/usr/local/bin/magick
30
+ - PEXELS_API_KEY=${PEXELS_API_KEY}
31
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
32
+ depends_on:
33
+ - frontend
34
+ restart: always
35
+
36
+ volumes:
37
+ files:
fonts/bold_font.ttf ADDED
Binary file (28.9 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ g4f==0.2.0.7
2
+ setuptools
3
+ wheel
4
+ requests==2.31.0
5
+ moviepy==1.0.3
6
+ termcolor==2.4.0
7
+ flask==3.0.0
8
+ flask-cors==4.0.0
9
+ playsound==1.3.0
10
+ Pillow==9.5.0
11
+ python-dotenv==1.0.0
12
+ srt_equalizer==0.1.8
13
+ platformdirs==4.1.0
14
+ undetected_chromedriver
15
+ assemblyai
16
+ brotli
17
+ google-api-python-client
18
+ oauth2client
19
+ openai
20
+ google-generativeai