├── .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 |
  1. 6 | Technology used 7 |
  2. 8 |
  3. Getting started
  4. 9 |
  5. File Contents of folder
  6. 10 |
  7. Learn More
  8. 11 |
  9. References
  10. 12 |
13 |
14 |
15 |
16 | 17 | Logo 18 | 19 | 20 |

Video Content Aggregator

21 | 22 | 23 |

24 | Video Content Generator with YouTube API, OpenAI API, and MoviePy. Create dynamic video content with a vc aggregator. 25 |
26 | Explore the docs » 27 |
28 |
29 | View Clips 30 | · 31 | Report Bug 32 | · 33 | Request Feature 34 |

35 |
36 | 37 | 38 | ## Technology used 39 | 40 | ![Python Badge](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=fff&style=for-the-badge) 41 | ![OpenAI API](https://img.shields.io/badge/OpenAI%20API-00A67E?style=for-the-badge&logo=openai&logoColor=white) 42 | ![YouTube API](https://img.shields.io/badge/YouTube%20API-FF0000?style=for-the-badge&logo=youtube&logoColor=white) 43 | ![Pexel API](https://img.shields.io/badge/Pexel%20API-05A081?style=for-the-badge&logo=pexels&logoColor=white) 44 | ![Google Cloud Platform](https://img.shields.io/badge/Google%20Cloud%20Platform-4285F4?style=for-the-badge&logo=google-cloud&logoColor=white) 45 | ![MoviePy](https://img.shields.io/badge/MoviePy-000000?style=for-the-badge&logo=python&logoColor=white) 46 | ![FFmpeg](https://img.shields.io/badge/FFmpeg-007ACC?style=for-the-badge&logo=ffmpeg&logoColor=white) 47 | ![Google Auth](https://img.shields.io/badge/Google%20Auth-4285F4?style=for-the-badge&logo=google&logoColor=white) 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 | --------------------------------------------------------------------------------