├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ └── greetings.yml
├── .gitignore
├── .vscode
└── settings.json
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── __pycache__
├── aggregate_fv2.cpython-311.pyc
└── upload_yt.cpython-311.pyc
├── aggregate_fv2.py
├── environment_variables.py
├── img
├── logo.png
└── vca.png
├── main.py
├── output_folder
└── note.txt
├── requirements.txt
├── test_api
├── gpt_prompt.py
└── youtube_video_data.py
├── test_script
└── test_aggregate.py
├── trending.py
└── upload_yt.py
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/greetings.yml:
--------------------------------------------------------------------------------
1 | name: Greetings
2 |
3 | on: [pull_request_target, issues]
4 |
5 | jobs:
6 | greeting:
7 | runs-on: ubuntu-latest
8 | permissions:
9 | issues: write
10 | pull-requests: write
11 | steps:
12 | - uses: actions/first-interaction@v1
13 | with:
14 | repo-token: ${{ secrets.GITHUB_TOKEN }}
15 | issue-message: "Message that will be displayed on users' first issue"
16 | pr-message: "Message that will be displayed on users' first pull request"
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | yt_client_secret.json
3 | token.json
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[python]": {
3 | "editor.defaultFormatter": "ms-python.autopep8"
4 | },
5 | "python.formatting.provider": "none"
6 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | gcho13@my.bcit.ca.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Gun-Hee David Cho
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Table of Contents
4 |
5 | -
6 | Technology used
7 |
8 | - Getting started
9 | - File Contents of folder
10 | - Learn More
11 | - References
12 |
13 |
14 |
15 |
36 |
37 |
38 | ## Technology used
39 |
40 | 
41 | 
42 | 
43 | 
44 | 
45 | 
46 | 
47 | 
48 |
49 |
50 | ## Getting Started
51 |
52 | 1. Clone the repo
53 | ```sh
54 | git clone https://github.com/gdcho/algo_v
55 | ```
56 | 2. Obtain API keys from [YouTube](https://developers.google.com/youtube/v3/getting-started), [OpenAI](https://beta.openai.com/), and [Pexels](https://www.pexels.com/api/new/) and save them in .env file
57 | 3. Install Python requirements
58 |
59 | ```sh
60 | pip install -r requirements.txt
61 | ```
62 | 4. Obtain OAuth Client Secret from [Google Cloud Platform](https://console.cloud.google.com/apis/credentials) and create yt_client_secret.json
63 | 5. Run the python script
64 |
65 | ```sh
66 | python3 main.py
67 | ```
68 |
69 | ## File Contents of folder
70 |
71 | ```
72 | 📦
73 | ├── README.md
74 | ├── __pycache__
75 | │ ├── aggregate_fv2.cpython-311.pyc
76 | │ └── upload_yt.cpython-311.pyc
77 | ├── aggregate_fv2.py
78 | ├── environment_variables.py
79 | ├── img
80 | │ ├── logo.png
81 | │ └── vca.png
82 | ├── main.py
83 | ├── output_folder
84 | │ └── note.txt
85 | ├── requirements.txt
86 | ├── test_api
87 | │ ├── gpt_prompt.py
88 | │ └── youtube_video_data.py
89 | ├── test_script
90 | │ └── test_aggregate.py
91 | ├── upload_yt.py
92 | └── yt_client_secret.json
93 | ```
94 |
95 | ## Learn More
96 |
97 | To learn more about Python, take a look at the following resources:
98 |
99 | - [Python Documentation](https://www.python.org/doc/) - learn about Python features and API.
100 | - [Python Tutorial](https://docs.python.org/3/tutorial/) - an interactive Python tutorial.
101 |
102 | To learn more about MoviePy, take a look at the following resources:
103 |
104 | - [MoviePy Documentation](https://zulko.github.io/moviepy/) - learn about MoviePy features and API.
105 | - [MoviePy Tutorial](https://zulko.github.io/moviepy/getting_started/your_first_clip.html) - an interactive MoviePy tutorial.
106 |
107 | To learn more about the APIs, take a look at the following resources:
108 |
109 | - [YouTube API](https://developers.google.com/youtube/v3/getting-started) - learn about YouTube API features and API.
110 | - [OpenAI API](https://beta.openai.com/) - learn about OpenAI API features and API.
111 | - [Pexels API](https://www.pexels.com/api/new/) - learn about Pexels API features and API.
112 |
113 | To learn more about Google Cloud Platform, take a look at the following resources:
114 |
115 | - [Google Cloud Platform](https://console.cloud.google.com/apis/credentials) - learn about Google Cloud Platform features and API.
116 | - [Google Cloud Platform Documentation](https://cloud.google.com/docs) - learn about Google Cloud Platform features and API.
117 |
118 |
119 | ## References
120 |
121 | [Python](https://www.python.org/) ·
122 | [MoviePy](https://zulko.github.io/moviepy/) ·
123 | [YouTube API](https://developers.google.com/youtube/v3/getting-started) ·
124 | [OpenAI API](https://beta.openai.com/) ·
125 | [Google Cloud Platform](https://console.cloud.google.com/apis/credentials) ·
126 | [Pexels API](https://www.pexels.com/api/new/)
127 |
--------------------------------------------------------------------------------
/__pycache__/aggregate_fv2.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdcho/vc_aggregator/d82809d9ed9dc0807167dd5b3497093ab0b1d46c/__pycache__/aggregate_fv2.cpython-311.pyc
--------------------------------------------------------------------------------
/__pycache__/upload_yt.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdcho/vc_aggregator/d82809d9ed9dc0807167dd5b3497093ab0b1d46c/__pycache__/upload_yt.cpython-311.pyc
--------------------------------------------------------------------------------
/aggregate_fv2.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | import openai
4 | import urllib.request
5 | import textwrap
6 | from dotenv import load_dotenv
7 | from gtts import gTTS
8 | from io import BytesIO
9 | from googleapiclient.discovery import build
10 | from moviepy.editor import (
11 | AudioFileClip,
12 | VideoFileClip,
13 | TextClip,
14 | CompositeVideoClip,
15 | concatenate_videoclips,
16 | )
17 | from pytube import YouTube
18 | import ssl
19 | from moviepy.editor import clips_array
20 | from moviepy.config import change_settings
21 | from concurrent.futures import ThreadPoolExecutor
22 | change_settings({"AUDIO_READING_FUNCTION": "pydub"})
23 |
24 | ssl._create_default_https_context = ssl._create_unverified_context
25 |
26 | os.environ["IMAGEIO_FFMPEG_EXE"] = "/usr/bin/ffmpeg"
27 | load_dotenv()
28 |
29 | OUTPUT_FOLDER = "//Users/davidcho/vc_aggregator/output_folder"
30 | API_KEY = os.getenv("OPENAI_API_KEY")
31 | PEXELS_API_KEY = os.getenv("PEXELS_API_KEY")
32 | YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY")
33 | openai.api_key = API_KEY
34 | youtube = build('youtube', 'v3', developerKey=YOUTUBE_API_KEY)
35 |
36 |
37 | def generate_fact():
38 | prompt = "Give me around 75 words based on an interesting fact."
39 | response = openai.Completion.create(
40 | engine="text-davinci-003", prompt=prompt, max_tokens=200)
41 | return response.choices[0].text.strip()
42 |
43 |
44 | def generate_title(fact):
45 | prompt = f"Based on the generated fact, {fact}, return a short title for the video."
46 | response = openai.Completion.create(
47 | engine="text-davinci-003", prompt=prompt, max_tokens=30)
48 | video_title = response.choices[0].text.strip()
49 | return video_title
50 |
51 |
52 | def generate_subject_noun(fact):
53 | prompt = f"Based on the generated fact, {fact}, return a main subject noun."
54 | response = openai.Completion.create(
55 | engine="text-davinci-003", prompt=prompt, max_tokens=30)
56 | return response.choices[0].text.strip()
57 |
58 |
59 | def fetch_pexels_videos(keyword):
60 | url = f"https://api.pexels.com/videos/search?query={keyword}&per_page=3"
61 | headers = {"Authorization": PEXELS_API_KEY}
62 | response = requests.get(url, headers=headers, timeout=30)
63 | return response.json().get('videos', [])
64 |
65 |
66 | def fetch_youtube_videos(keyword):
67 | search_query = keyword + " free stock footage"
68 | request = youtube.search().list(q=search_query, part='snippet', maxResults=3)
69 | response = request.execute()
70 | return response.get('items', [])
71 |
72 |
73 | def download_video_from_url(url, filename):
74 | urllib.request.urlretrieve(url, filename)
75 |
76 |
77 | def get_tts_audio_clip(text):
78 | tts = gTTS(text, lang='en', tld='com.au', slow=False)
79 | audio_bytes = BytesIO()
80 | tts.write_to_fp(audio_bytes)
81 | audio_bytes.seek(0)
82 | temp_audio_filename = os.path.join(OUTPUT_FOLDER, "temp_audio.mp3")
83 | with open(temp_audio_filename, "wb") as f:
84 | f.write(audio_bytes.read())
85 | audio_clip = AudioFileClip(temp_audio_filename)
86 | return audio_clip
87 |
88 |
89 | def process_pexels_videos(pexels_videos, audio_clip_duration):
90 | video_clips = []
91 | target_duration = audio_clip_duration / 3
92 |
93 | for video_info in pexels_videos:
94 | video_files = video_info.get('video_files', [])
95 | if video_files:
96 | video_url = next(
97 | (file['link'] for file in video_files if file['file_type'] == 'video/mp4'), None)
98 | if video_url:
99 | video_filename = os.path.basename(
100 | urllib.parse.urlparse(video_url).path)
101 | video_filename = os.path.join(OUTPUT_FOLDER, video_filename)
102 | download_video_from_url(video_url, video_filename)
103 | video_clip = VideoFileClip(video_filename)
104 |
105 | if video_clip.duration < target_duration:
106 | loop_count = int(target_duration //
107 | video_clip.duration) + 1
108 | video_clip = concatenate_videoclips(
109 | [video_clip] * loop_count)
110 |
111 | video_clip = video_clip.set_duration(target_duration)
112 | video_clips.append(video_clip)
113 |
114 | return video_clips
115 |
116 |
117 | def process_youtube_videos(youtube_videos, audio_clip_duration):
118 | video_clips = []
119 | target_duration = audio_clip_duration / 3
120 |
121 | for video_info in youtube_videos:
122 | video_id = video_info.get('id', {}).get('videoId')
123 | if video_id:
124 | youtube_video_url = f"https://www.youtube.com/watch?v={video_id}"
125 | yt = YouTube(youtube_video_url)
126 | video_stream = yt.streams.filter(
127 | progressive=True, file_extension="mp4").order_by("resolution").desc().first()
128 | video_filename = os.path.join(
129 | OUTPUT_FOLDER, f"youtube_video_{video_id}.mp4")
130 | video_stream.download(output_path=OUTPUT_FOLDER,
131 | filename=os.path.basename(video_filename))
132 | video_clip = VideoFileClip(video_filename).subclip(5)
133 |
134 | if video_clip.duration < target_duration:
135 | loop_count = int(target_duration // video_clip.duration) + 1
136 | video_clip = concatenate_videoclips([video_clip] * loop_count)
137 |
138 | video_clip = video_clip.set_duration(target_duration)
139 | video_clips.append(video_clip)
140 | return video_clips
141 |
142 |
143 | def resize_and_crop_video(clip, target_width, target_height):
144 | original_aspect_ratio = clip.size[0] / clip.size[1]
145 | target_aspect_ratio = target_width / target_height
146 |
147 | if original_aspect_ratio > target_aspect_ratio:
148 | new_width = int(clip.size[1] * target_aspect_ratio)
149 | new_height = clip.size[1]
150 | else:
151 | new_width = clip.size[0]
152 | new_height = int(clip.size[0] / target_aspect_ratio)
153 |
154 | x_center = clip.size[0] / 2
155 | y_center = clip.size[1] / 2
156 | clip_cropped = clip.crop(
157 | x_center=x_center,
158 | y_center=y_center,
159 | width=new_width,
160 | height=new_height
161 | )
162 |
163 | return clip_cropped.resize(newsize=(target_width, target_height))
164 |
165 |
166 | def generate_subtitles(fact, final_video_duration):
167 | fact_parts = textwrap.wrap(fact, width=40)
168 | subs = []
169 | interval_duration = 2.95
170 | start_time = 0
171 | for part in fact_parts:
172 | end_time = min(start_time + interval_duration, final_video_duration)
173 | subs.append(((start_time, end_time), part))
174 | start_time = end_time
175 | return subs
176 |
177 |
178 | def annotate_video_with_subtitles(video, subtitles):
179 | def annotate(clip, txt, txt_color="white", fontsize=50, font="Xolonium-Bold"):
180 | txtclip = TextClip(txt, fontsize=fontsize, color=txt_color,
181 | font=font, bg_color="black").set_duration(clip.duration)
182 | txtclip = txtclip.set_position(
183 | ("center", "center")).set_duration(clip.duration)
184 | cvc = CompositeVideoClip([clip, txtclip])
185 | return cvc
186 |
187 | annotated_clips = [annotate(video.subclip(from_t, min(
188 | to_t, video.duration)), txt) for (from_t, to_t), txt in subtitles]
189 | return concatenate_videoclips(annotated_clips)
190 |
191 |
192 | def delete_output_files_except_final(video_title):
193 | final_video_name = video_title + ".mp4"
194 | for filename in os.listdir(OUTPUT_FOLDER):
195 | file_path = os.path.join(OUTPUT_FOLDER, filename)
196 | if os.path.isfile(file_path) and filename != final_video_name:
197 | os.remove(file_path)
198 |
199 |
200 | def main():
201 | fact = generate_fact()
202 | video_title = generate_title(fact)
203 | noun = generate_subject_noun(fact)
204 |
205 | video_width = 1080
206 | video_height = 1920
207 |
208 | with ThreadPoolExecutor() as executor:
209 | future_pexels = executor.submit(fetch_pexels_videos, noun)
210 | future_youtube = executor.submit(fetch_youtube_videos, noun)
211 | future_audio = executor.submit(get_tts_audio_clip, fact)
212 |
213 | pexels_videos = future_pexels.result()
214 | youtube_videos = future_youtube.result()
215 | audio_clip = future_audio.result()
216 |
217 | print(f"\nGenerated Fact: {fact}")
218 | print(f"\nGenerated Noun: {noun}\n")
219 | for video in pexels_videos:
220 | print(f"Pexel URL: {video['url']}")
221 | for video_info in youtube_videos:
222 | video_id = video_info['id'].get('videoId')
223 | if video_id:
224 | print(f"YouTube Video Link: https://www.youtube.com/watch?v={video_id}")
225 |
226 | audio_clip = audio_clip.volumex(1.0)
227 |
228 | with ThreadPoolExecutor() as executor:
229 | future_pexels_process = executor.submit(process_pexels_videos, pexels_videos, audio_clip.duration)
230 | future_youtube_process = executor.submit(process_youtube_videos, youtube_videos, audio_clip.duration)
231 |
232 | pexels_video_clips = future_pexels_process.result()
233 | youtube_video_clips = future_youtube_process.result()
234 |
235 | video_clips = pexels_video_clips + youtube_video_clips
236 |
237 | # Parallelize
238 | with ThreadPoolExecutor() as executor:
239 | resized_cropped_clips = list(executor.map(lambda clip: resize_and_crop_video(clip, video_width, video_height), video_clips))
240 |
241 | paired_clips = list(zip(resized_cropped_clips[:len(resized_cropped_clips)//2],
242 | resized_cropped_clips[len(resized_cropped_clips)//2:]))
243 |
244 | stacked_clips = [clips_array([[clip1], [clip2]])
245 | for clip1, clip2 in paired_clips]
246 |
247 | final_video = concatenate_videoclips(stacked_clips, method="compose")
248 |
249 | final_video_duration = min(audio_clip.duration, final_video.duration)
250 | final_video = final_video.set_audio(audio_clip.subclip(0, final_video_duration))
251 |
252 | output_video_path = os.path.join(OUTPUT_FOLDER, "final_video_shorts.mp4")
253 | final_video.write_videofile(output_video_path, codec="libx264", audio_codec="aac", threads=4)
254 |
255 | subtitles = generate_subtitles(fact, final_video_duration)
256 | final_video_with_subs = annotate_video_with_subtitles(final_video, subtitles)
257 | final_video_with_subs = final_video_with_subs.set_audio(final_video.audio)
258 |
259 | output_video_with_subs_path = os.path.join(OUTPUT_FOLDER, video_title + ".mp4")
260 | final_video_with_subs.write_videofile(output_video_with_subs_path, codec="libx264", audio_codec="aac", threads=4)
261 |
262 | delete_output_files_except_final(video_title)
263 |
264 | audio_clip.close()
265 | final_video.close()
266 | final_video_with_subs.close()
267 |
268 | if __name__ == "__main__":
269 | main()
270 |
--------------------------------------------------------------------------------
/environment_variables.py:
--------------------------------------------------------------------------------
1 | from dotenv import load_dotenv
2 | import os
3 |
4 | load_dotenv()
5 |
6 | tiktok_api_key = os.getenv("TIKTOK_API_KEY")
7 | youtube_api_key = os.getenv("YOUTUBE_API_KEY")
8 | pexels_api_key = os.getenv("PEXELS_API_KEY")
9 | openai_api_key = os.getenv("OPENAI_API_KEY")
10 |
--------------------------------------------------------------------------------
/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdcho/vc_aggregator/d82809d9ed9dc0807167dd5b3497093ab0b1d46c/img/logo.png
--------------------------------------------------------------------------------
/img/vca.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdcho/vc_aggregator/d82809d9ed9dc0807167dd5b3497093ab0b1d46c/img/vca.png
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from aggregate_fv2 import main as generate_video
2 | from upload_yt import main as upload_to_youtube
3 |
4 | def main():
5 | generate_video()
6 | upload_to_youtube()
7 |
8 | if __name__ == "__main__":
9 | main()
10 |
--------------------------------------------------------------------------------
/output_folder/note.txt:
--------------------------------------------------------------------------------
1 | This is the output_folder.
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | moviepy
2 | google-api-python-client
3 | openai
4 | gtts
5 | python-dotenv
6 | requests
7 | shutilwhich
8 | urllib3
9 | pytube
10 | opencv-python
11 | google-auth-httplib2
12 | google-auth-oauthlib
13 |
--------------------------------------------------------------------------------
/test_api/gpt_prompt.py:
--------------------------------------------------------------------------------
1 | import openai
2 | from dotenv import load_dotenv
3 | import os
4 | import requests
5 |
6 | load_dotenv()
7 | api_key = os.getenv("OPENAI_API_KEY")
8 | openai.api_key = api_key
9 | pexels_api_key = os.getenv("PEXELS_API_KEY")
10 |
11 | def generate_fact_prompt():
12 | return "Give me around 75 words based on an interesting fact."
13 |
14 |
15 | def generate_subject_noun_prompt(fact):
16 | return f"Based on the generated fact, {fact}, return a main subject noun."
17 |
18 |
19 | def fetch_pexels_videos(keyword):
20 | pexels_url = f"https://api.pexels.com/videos/search?query={keyword}&per_page=2"
21 | headers = {"Authorization": pexels_api_key}
22 | response = requests.get(pexels_url, headers=headers)
23 | pexels_data = response.json()
24 | return pexels_data.get('videos', [])
25 |
26 |
27 | fact_prompt = generate_fact_prompt()
28 | fact_response = openai.Completion.create(
29 | engine="text-davinci-003",
30 | prompt=fact_prompt,
31 | max_tokens=200,
32 | stop=None
33 | )
34 | generated_fact = fact_response.choices[0].text.strip()
35 |
36 | noun_prompt = generate_subject_noun_prompt(generated_fact)
37 | noun_response = openai.Completion.create(
38 | engine="text-davinci-003",
39 | prompt=noun_prompt,
40 | max_tokens=30,
41 | stop=None
42 | )
43 | generated_noun = noun_response.choices[0].text.strip()
--------------------------------------------------------------------------------
/test_api/youtube_video_data.py:
--------------------------------------------------------------------------------
1 | from googleapiclient.discovery import build
2 | from dotenv import load_dotenv
3 | import os
4 |
5 | load_dotenv()
6 |
7 | youtube_api_key = os.getenv("YOUTUBE_API_KEY")
8 |
9 | youtube = build('youtube', 'v3', developerKey=youtube_api_key)
10 |
11 | # Fetch video data
12 | search_query = 'cats'
13 | request = youtube.search().list(q=search_query, part='snippet', maxResults=10)
14 | response = request.execute()
15 |
16 | for item in response['items']:
17 | print(item['snippet']['title'])
18 | print(item['snippet']['description'])
19 |
20 | ## working
--------------------------------------------------------------------------------
/test_script/test_aggregate.py:
--------------------------------------------------------------------------------
1 | import openai
2 | import requests
3 | from dotenv import load_dotenv
4 | import os
5 | from gtts import gTTS
6 | from io import BytesIO
7 | from googleapiclient.discovery import build
8 | from moviepy.editor import (
9 | AudioFileClip,
10 | VideoFileClip,
11 | CompositeVideoClip,
12 | concatenate_videoclips,
13 | )
14 | import urllib.request
15 | import ssl
16 | from pytube import YouTube
17 | from moviepy.editor import VideoFileClip as EditorVideoFileClip
18 | ssl._create_default_https_context = ssl._create_unverified_context
19 |
20 | os.environ["IMAGEIO_FFMPEG_EXE"] = "/usr/bin/ffmpeg"
21 |
22 | output_folder = "//Users/davidcho/vc_aggregator/output_folder"
23 |
24 | load_dotenv()
25 | api_key = os.getenv("OPENAI_API_KEY")
26 | pexels_api_key = os.getenv("PEXELS_API_KEY")
27 | openai.api_key = api_key
28 | youtube_api_key = os.getenv("YOUTUBE_API_KEY")
29 | youtube = build('youtube', 'v3', developerKey=youtube_api_key)
30 |
31 |
32 | def generate_fact_prompt():
33 | return "Give me around 75 words based on an interesting fact."
34 |
35 |
36 | def generate_subject_noun_prompt(fact):
37 | return f"Based on the generated fact, {fact}, return a main subject noun."
38 |
39 |
40 | def fetch_pexels_videos(keyword):
41 | pexels_url = f"https://api.pexels.com/videos/search?query={keyword}&per_page=3"
42 | headers = {"Authorization": pexels_api_key}
43 | response = requests.get(pexels_url, headers=headers, timeout=30)
44 | pexels_data = response.json()
45 | return pexels_data.get('videos', [])
46 |
47 |
48 | fact_prompt = generate_fact_prompt()
49 | fact_response = openai.Completion.create(
50 | engine="text-davinci-003",
51 | prompt=fact_prompt,
52 | max_tokens=200,
53 | stop=None
54 | )
55 | generated_fact = fact_response.choices[0].text.strip()
56 |
57 | noun_prompt = generate_subject_noun_prompt(generated_fact)
58 | noun_response = openai.Completion.create(
59 | engine="text-davinci-003",
60 | prompt=noun_prompt,
61 | max_tokens=30,
62 | stop=None
63 | )
64 | generated_noun = noun_response.choices[0].text.strip()
65 |
66 | # Fetch Pexels
67 | videos = fetch_pexels_videos(generated_noun)
68 |
69 | print(f"\nGenerated Fact: {generated_fact}")
70 | print(f"\nGenerated Noun: {generated_noun}\n")
71 | if videos:
72 | for video in videos:
73 | print(f"Pexel URL: {video['url']}")
74 | else:
75 | print("No videos found.")
76 |
77 | # Fetch YouTube
78 | search_query = generated_noun + " free stock footage"
79 | request = youtube.search().list(q=search_query, part='snippet', maxResults=3)
80 | response = request.execute()
81 | print("\nYouTube:")
82 | if 'items' in response:
83 | for video_info in response['items']:
84 | video_id = video_info['id'].get('videoId')
85 | if video_id:
86 | print(
87 | f"YouTube Video Link: https://www.youtube.com/watch?v={video_id}")
88 | else:
89 | print("No 'videoId' found for this item.")
90 | else:
91 | print("No videos found.")
92 |
93 | # gTTS - Text to Speech
94 | tts = gTTS(generated_fact, lang='en', tld='com.au', slow=False)
95 | audio_bytes = BytesIO()
96 | tts.write_to_fp(audio_bytes)
97 | audio_bytes.seek(0)
98 |
99 | temp_audio_filename = "temp_audio.mp3"
100 | with open(temp_audio_filename, "wb") as f:
101 | f.write(audio_bytes.read())
102 |
103 | tts_audio_clip = AudioFileClip(temp_audio_filename)
104 | os.remove(temp_audio_filename)
105 |
106 | # Fetch Pexels
107 | pexels_videos = fetch_pexels_videos(generated_noun)
108 | pexels_video_clips = []
109 |
110 | for video_info in pexels_videos:
111 | video_files = video_info.get('video_files', [])
112 |
113 | if video_files:
114 | video_url = next(
115 | (file['link'] for file in video_files if file['file_type'] == 'video/mp4'), None)
116 |
117 | if video_url:
118 | video_filename = os.path.basename(
119 | urllib.parse.urlparse(video_url).path)
120 | video_filename = os.path.join(output_folder, video_filename)
121 | urllib.request.urlretrieve(video_url, video_filename)
122 | pexels_video_clip = VideoFileClip(video_filename)
123 | pexels_video_clip = pexels_video_clip.subclip(
124 | 0, tts_audio_clip.duration//3).set_duration(tts_audio_clip.duration//3)
125 | pexels_video_clips.append(pexels_video_clip)
126 |
127 | # Fetch YouTube
128 | youtube_video_clips = []
129 |
130 | for video_info in response.get('items', []):
131 | video_id = video_info.get('id', {}).get('videoId')
132 |
133 | if video_id:
134 | youtube_video_url = f"https://www.youtube.com/watch?v={video_id}"
135 |
136 | yt = YouTube(youtube_video_url)
137 | video_stream = yt.streams.filter(
138 | progressive=True, file_extension="mp4").order_by("resolution").desc().first()
139 |
140 | video_filename = os.path.join(
141 | output_folder, f"youtube_video_{video_id}.mp4")
142 |
143 | video_stream.download(output_path=output_folder,
144 | filename=os.path.basename(video_filename))
145 |
146 | youtube_video_clip = VideoFileClip(video_filename)
147 | youtube_video_clip = youtube_video_clip.subclip(
148 | 0, tts_audio_clip.duration//3).set_duration(tts_audio_clip.duration//3)
149 | youtube_video_clips.append(youtube_video_clip)
150 |
151 | for pexels_clip in pexels_video_clips:
152 | pexels_clip = pexels_clip.set_duration(tts_audio_clip.duration//3)
153 |
154 | for youtube_clip in youtube_video_clips:
155 | youtube_clip = youtube_clip.set_duration(tts_audio_clip.duration//3)
156 |
157 | video_width = 1080
158 | video_height = 1920
159 |
160 | stacked_pexels_videos = concatenate_videoclips(
161 | pexels_video_clips, method="compose")
162 | stacked_pexels_videos = stacked_pexels_videos.resize(
163 | (video_width, video_height))
164 |
165 | stacked_youtube_videos = concatenate_videoclips(
166 | youtube_video_clips, method="compose")
167 | stacked_youtube_videos = stacked_youtube_videos.resize(
168 | (video_width, video_height))
169 |
170 |
171 | final_video = CompositeVideoClip(
172 | [stacked_pexels_videos, stacked_youtube_videos], size=(video_width, video_height))
173 |
174 | final_video_duration = min(tts_audio_clip.duration, final_video.duration)
175 | final_video = final_video.set_audio(tts_audio_clip.subclip(0, final_video_duration))
176 |
177 | output_video_path = os.path.join(output_folder, "final_video_shorts.mp4")
178 | final_video.write_videofile(
179 | output_video_path, codec="libx264", audio_codec="aac", threads=4)
180 |
--------------------------------------------------------------------------------
/trending.py:
--------------------------------------------------------------------------------
1 | from google.oauth2.credentials import Credentials
2 | from google_auth_oauthlib.flow import InstalledAppFlow
3 | from google.auth.transport.requests import Request
4 | from googleapiclient.discovery import build
5 |
6 | def get_youtube_service():
7 | creds = None
8 | SCOPES = ["https://www.googleapis.com/auth/youtube.readonly"]
9 | API_VERSION = "v3"
10 | API_SERVICE_NAME = "youtube"
11 |
12 | try:
13 | creds = Credentials.from_authorized_user_file("token.json", SCOPES)
14 | except FileNotFoundError:
15 | flow = InstalledAppFlow.from_client_secrets_file("yt_client_secret.json", SCOPES)
16 | creds = flow.run_local_server(port=0)
17 |
18 | with open("token.json", "w") as token:
19 | token.write(creds.to_json())
20 |
21 | return build(API_SERVICE_NAME, API_VERSION, credentials=creds)
22 |
23 | def get_trending_videos(youtube):
24 | request = youtube.videos().list(
25 | part="snippet",
26 | chart="mostPopular",
27 | regionCode="US",
28 | maxResults=5
29 | )
30 | response = request.execute()
31 |
32 | for item in response["items"]:
33 | print(f"Title: {item['snippet']['title']}")
34 | print("-----")
35 |
36 | if __name__ == "__main__":
37 | youtube = get_youtube_service()
38 | get_trending_videos(youtube)
39 |
--------------------------------------------------------------------------------
/upload_yt.py:
--------------------------------------------------------------------------------
1 | import os
2 | import google_auth_oauthlib.flow
3 | import googleapiclient.discovery
4 | import googleapiclient.errors
5 | from googleapiclient.http import MediaFileUpload
6 | from concurrent.futures import ThreadPoolExecutor
7 |
8 | API_SERVICE_NAME = "youtube"
9 | API_VERSION = "v3"
10 | CLIENT_SECRETS_FILE = "yt_client_secret.json"
11 | OAUTH_SCOPE = ["https://www.googleapis.com/auth/youtube.upload"]
12 |
13 |
14 | def authenticate_youtube():
15 | os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
16 |
17 | flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(
18 | CLIENT_SECRETS_FILE, OAUTH_SCOPE)
19 | credentials = flow.run_local_server(port=0)
20 |
21 | youtube = googleapiclient.discovery.build(
22 | API_SERVICE_NAME, API_VERSION, credentials=credentials)
23 | return youtube
24 |
25 |
26 | def upload_video_to_youtube(youtube, file_path, title, description):
27 | request = youtube.videos().insert(
28 | part="snippet,status",
29 | body={
30 | "snippet": {
31 | "categoryId": "22",
32 | "description": description,
33 | "title": title
34 | },
35 | "status": {
36 | "privacyStatus": "private",
37 | "selfDeclaredMadeForKids": False,
38 | "publishAt": "2023-08-24T00:00:00.0Z"
39 | }
40 | },
41 | media_body=MediaFileUpload(
42 | file_path, mimetype='video/mp4', resumable=True)
43 | )
44 | return request.execute()
45 |
46 |
47 | def get_only_video_from_folder(folder_path):
48 | video_files = [f for f in os.listdir(folder_path) if f.endswith('.mp4')]
49 |
50 | if len(video_files) == 1:
51 | return os.path.join(folder_path, video_files[0])
52 | else:
53 | print(
54 | f"Found {len(video_files)} video files in the folder. Expected only 1.")
55 | return None
56 |
57 |
58 | def get_video_title_from_filepath(file_path):
59 | return os.path.splitext(os.path.basename(file_path))[0]
60 |
61 |
62 | def main():
63 | with ThreadPoolExecutor() as executor:
64 | youtube = executor.submit(authenticate_youtube).result()
65 | file_path = executor.submit(
66 | get_only_video_from_folder, "//Users/davidcho/vc_aggregator/output_folder").result()
67 |
68 | if file_path:
69 | title = get_video_title_from_filepath(file_path)
70 | description = ("Quick Unique Facts: "
71 | "#quickuniquefacts #uniquefacts #funfacts "
72 | "#interestingfacts #factoftheday #didyouknow "
73 | "#knowledge #trivia #dailyfacts ")
74 |
75 | response = upload_video_to_youtube(
76 | youtube, file_path, title, description)
77 | print(response)
78 | else:
79 | print("Video upload aborted due to file path issues.")
80 |
81 |
82 | if __name__ == "__main__":
83 | main()
84 |
--------------------------------------------------------------------------------