├── .gitignore ├── .idea └── vcs.xml ├── Content.py ├── LICENSE ├── README.md ├── Sentence.py ├── config.py.example ├── services ├── ImageProcessing.py ├── TextProcessing.py ├── UserInput.py ├── VideoProcessing.py ├── Youtube.py └── __init__.py └── video-maker.py /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | content.json 3 | 4 | *.pickle 5 | *.jpg 6 | *.png 7 | *.mp4 8 | *.mp3 9 | client_secrets.json 10 | *oauth2.json 11 | 12 | .DS_Store 13 | 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # celery beat schedule file 92 | celerybeat-schedule 93 | 94 | # SageMath parsed files 95 | *.sage.py 96 | 97 | # Environments 98 | .env 99 | .venv 100 | env/ 101 | venv/ 102 | ENV/ 103 | env.bak/ 104 | venv.bak/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | 119 | *.pyc 120 | 121 | ### PyCharm ### 122 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 123 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 124 | 125 | # User-specific stuff 126 | .idea/**/workspace.xml 127 | .idea/**/tasks.xml 128 | .idea/**/usage.statistics.xml 129 | .idea/**/dictionaries 130 | .idea/**/shelf 131 | 132 | # Generated files 133 | .idea/**/contentModel.xml 134 | 135 | # Sensitive or high-churn files 136 | .idea/**/dataSources/ 137 | .idea/**/dataSources.ids 138 | .idea/**/dataSources.local.xml 139 | .idea/**/sqlDataSources.xml 140 | .idea/**/dynamic.xml 141 | .idea/**/uiDesigner.xml 142 | .idea/**/dbnavigator.xml 143 | 144 | # Gradle 145 | .idea/**/gradle.xml 146 | .idea/**/libraries 147 | 148 | # Gradle and Maven with auto-import 149 | # When using Gradle or Maven with auto-import, you should exclude module files, 150 | # since they will be recreated, and may cause churn. Uncomment if using 151 | # auto-import. 152 | # .idea/modules.xml 153 | # .idea/*.iml 154 | # .idea/modules 155 | 156 | # CMake 157 | cmake-build-*/ 158 | 159 | # Mongo Explorer plugin 160 | .idea/**/mongoSettings.xml 161 | 162 | # File-based project format 163 | *.iws 164 | 165 | # IntelliJ 166 | out/ 167 | 168 | # mpeltonen/sbt-idea plugin 169 | .idea_modules/ 170 | 171 | # JIRA plugin 172 | atlassian-ide-plugin.xml 173 | 174 | # Cursive Clojure plugin 175 | .idea/replstate.xml 176 | 177 | # Crashlytics plugin (for Android Studio and IntelliJ) 178 | com_crashlytics_export_strings.xml 179 | crashlytics.properties 180 | crashlytics-build.properties 181 | fabric.properties 182 | 183 | # Editor-based Rest Client 184 | .idea/httpRequests 185 | 186 | # Android studio 3.1+ serialized cache file 187 | .idea/caches/build_file_checksums.ser 188 | 189 | # JetBrains templates 190 | **___jb_tmp___ 191 | 192 | ### PyCharm Patch ### 193 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 194 | 195 | *.iml 196 | modules.xml 197 | .idea/misc.xml 198 | *.ipr 199 | 200 | # Sonarlint plugin 201 | .idea/sonarlint 202 | 203 | # End of https://www.gitignore.io/api/macos,pycharm -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Content.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Sentence import Sentence 3 | import pickle 4 | 5 | 6 | class Content: 7 | def __init__(self): 8 | self.search_term = None 9 | self.prefix = None 10 | self.source_content = None 11 | self.source_content_sanitized = None 12 | self.sentences = [] 13 | self.downloaded_images = [] 14 | 15 | def to_json(self): 16 | return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) 17 | 18 | def add_sentence(self, sentence): 19 | self.sentences.append(sentence) 20 | 21 | def load_json(self, file_path): 22 | with open(file_path, 'r') as content_data: 23 | self.__dict__ = json.load(content_data) 24 | if len(self.sentences) > 0: 25 | for key, sentence in enumerate(self.sentences, start=0): 26 | new_sentence = Sentence() 27 | new_sentence.text = sentence["text"] 28 | new_sentence.images = sentence["images"] 29 | new_sentence.keywords = sentence["keywords"] 30 | self.sentences[key] = new_sentence 31 | 32 | @staticmethod 33 | def load(file_path): 34 | with open(file_path, 'rb') as fp: 35 | return pickle.load(fp) 36 | 37 | def save(self, file_path): 38 | with open(file_path, 'wb') as fp: 39 | pickle.dump(self, fp, protocol=pickle.HIGHEST_PROTOCOL) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Miguel Alemán 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 | # video-maker-bot-python 2 | A bot that can create a video about a topic and publish it on Youtube. 3 | -------------------------------------------------------------------------------- /Sentence.py: -------------------------------------------------------------------------------- 1 | class Sentence: 2 | def __init__(self): 3 | self.text = None 4 | self.keywords = [] 5 | self.images = [] 6 | 7 | def add_keyword(self, keyword): 8 | self.keywords.append(keyword) 9 | 10 | def add_image(self, image): 11 | self.images.append(image) 12 | -------------------------------------------------------------------------------- /config.py.example: -------------------------------------------------------------------------------- 1 | WATSON_CONFIG = { 2 | 'version': '2018-04-05', 3 | 'url': 'https://gateway.watsonplatform.net/natural-language-understanding/api', 4 | 'iam_apikey': 'YOUR_API_KEY' 5 | } 6 | 7 | ALGORITHMIA_CONFIG = { 8 | 'client': 'YOUR_API_KEY' 9 | } -------------------------------------------------------------------------------- /services/ImageProcessing.py: -------------------------------------------------------------------------------- 1 | from googleapiclient.discovery import build 2 | import config 3 | import wget 4 | 5 | 6 | def fetch_images_links(query): 7 | service = build("customsearch", "v1", developerKey=config.GOOGLE_CLOUD_CONFIG["api_key"]) 8 | 9 | response = service.cse().list( 10 | q=query, 11 | cx=config.GOOGLE_CLOUD_CONFIG["cse_id"], 12 | searchType="image", 13 | num=3, 14 | imgSize="medium" 15 | ).execute() 16 | links = [] 17 | for item in response['items']: 18 | links.append(item['link']) 19 | return links 20 | 21 | 22 | def fetch_images_and_download_for_sentences(sentences, search_term, downloaded_images): 23 | print("Searching images...") 24 | for key, sentence in enumerate(sentences, start=0): 25 | print("Searching images for \"{}\"...".format(generate_search_term(search_term, sentence, key))) 26 | sentence.images = fetch_images_links(generate_search_term(search_term, sentence, key)) 27 | print("Downloading images...") 28 | downloaded_images.append(download_single_image_for_sentence(sentence, key, downloaded_images)) 29 | return sentences, downloaded_images 30 | 31 | 32 | def generate_search_term(search_term, sentence, key): 33 | if key == 0: 34 | return sentence.keywords[0] 35 | return "{} {}".format(search_term, sentence.keywords[0]) 36 | 37 | 38 | def download_image(image_url, destination): 39 | print("Downlading image... {}".format(image_url)) 40 | try: 41 | wget.download(image_url, destination) 42 | return True 43 | except: 44 | print("An exception occurred") 45 | 46 | 47 | def download_single_image_for_sentence(sentence, sentence_key, downloaded_images): 48 | for image_url in sentence.images: 49 | print("Image to download... {}".format(image_url)) 50 | if image_url not in downloaded_images and download_image(image_url, "{}-original.png".format(sentence_key)): 51 | return image_url 52 | -------------------------------------------------------------------------------- /services/TextProcessing.py: -------------------------------------------------------------------------------- 1 | import Algorithmia 2 | import re 3 | from ibm_watson import NaturalLanguageUnderstandingV1 4 | from ibm_watson.natural_language_understanding_v1 import Features, KeywordsOptions 5 | import nltk 6 | from Sentence import Sentence 7 | import config 8 | 9 | DEFAULT_LANG = "en" 10 | MAXIMUM_SENTENCES_TO_EXTRACT = 7 11 | MINIMUM_KEYWORD_RELEVANCE = 0.60 12 | ALGORITHMIA_ALGO_VERSION = "web/WikipediaParser/0.1.2" 13 | 14 | 15 | def fetch_content_and_sanitize(prefix, search_term): 16 | print("Searching for content...") 17 | original_content = fetch_content_from_source("{} {}".format(prefix, search_term), DEFAULT_LANG) 18 | print("Sanitizing content...") 19 | sanitized_content = clean_dates_in_parenthesis(clean_empty_and_markup_lines(original_content)) 20 | return [original_content, sanitized_content] 21 | 22 | 23 | def clean_empty_and_markup_lines(string): 24 | return " ".join([line for line in string.split("\n") if len(line.strip()) > 0 and line[0] != "="]) 25 | 26 | 27 | def clean_dates_in_parenthesis(string): 28 | return re.sub("\((?:\([^()]*\)|[^()])*\)", "", string).replace(" ", " ") 29 | 30 | 31 | def fetch_content_from_source(article_name, lang): 32 | client = Algorithmia.client(config.ALGORITHMIA_CONFIG["client"]) 33 | algo = client.algo(ALGORITHMIA_ALGO_VERSION) 34 | return algo.pipe({ 35 | "articleName": article_name, 36 | "lang": lang 37 | }).result["content"] 38 | 39 | 40 | def extract_sentences_from_content(content, number_of_sentences): 41 | tokenizer = nltk.tokenize.PunktSentenceTokenizer() 42 | return tokenizer.tokenize(content)[:number_of_sentences] 43 | 44 | 45 | def fetch_keywords_from_sentence(sentence): 46 | nlu = NaturalLanguageUnderstandingV1(version=config.WATSON_CONFIG["version"], 47 | url=config.WATSON_CONFIG["url"], 48 | iam_apikey=config.WATSON_CONFIG["iam_apikey"]) 49 | response = nlu.analyze(text=sentence, features=Features(keywords=KeywordsOptions())) 50 | return extract_text_from_keywords_list(response.result["keywords"]) 51 | 52 | 53 | def extract_text_from_keywords_list(keywords): 54 | return [keyword["text"] for keyword in keywords if keyword["relevance"] > MINIMUM_KEYWORD_RELEVANCE] 55 | 56 | 57 | def fetch_keywords_from_content(content): 58 | sentence_list = [] 59 | for sentence in extract_sentences_from_content(content, MAXIMUM_SENTENCES_TO_EXTRACT): 60 | new_sentence = Sentence() 61 | new_sentence.text = sentence 62 | new_sentence.keywords = fetch_keywords_from_sentence(sentence) 63 | sentence_list.append(new_sentence) 64 | return sentence_list 65 | -------------------------------------------------------------------------------- /services/UserInput.py: -------------------------------------------------------------------------------- 1 | PREFIXES = [ 2 | "Who is", 3 | "What is", 4 | "History of" 5 | ] 6 | 7 | TOPIC_QUESTION = "Define a topic " 8 | PREFIX_QUESTION = "Define a prefix" 9 | 10 | 11 | def _generate_prefixes_option_string(prefixes): 12 | prefixes_string = "" 13 | for key, prefix_string in enumerate(prefixes, start=1): 14 | prefixes_string = "{} {} {} \n".format(prefixes_string, str(key), prefix_string) 15 | return prefixes_string 16 | 17 | 18 | def get_topic_and_prefix_from_input(): 19 | topic_input = input("{}\n".format(TOPIC_QUESTION)) 20 | prefix_key_input = input("{}\n{}".format(PREFIX_QUESTION, _generate_prefixes_option_string(PREFIXES))) 21 | 22 | return [str(topic_input), PREFIXES[int(prefix_key_input) - 1]] 23 | -------------------------------------------------------------------------------- /services/VideoProcessing.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageFilter, ImageFont, ImageDraw 2 | import textwrap 3 | from moviepy.editor import ImageClip, transfx, CompositeVideoClip, concatenate 4 | 5 | DEFAULT_FONT_TYPE = "Arial Bold.ttf" 6 | DEFAULT_FONT_SIZE = 48 7 | DEFAULT_TEXT_WIDTH = 75 8 | DEFAULT_SHADOW_OFFSET = 2 9 | ORIGINAL_IMAGE_SUFFIX = "-original.png" 10 | CONVERTED_IMAGE_SUFFIX = "-converted.png" 11 | SENTENCE_IMAGE_TAG = "-sentence" 12 | YOUTUBE_THUMBNAIL_PATH = "youtube_thumbnail.jpg" 13 | DEFAULT_IMAGE_WIDTH = 1920 14 | DEFAULT_IMAGE_HEIGHT = 1080 15 | DEFAULT_IMAGE_MODE = "RGBA" 16 | YOUTUBE_THUMBNAIL_MODE = "RGB" 17 | TEXT_POSITION = [(100, 50), (100, DEFAULT_IMAGE_HEIGHT - 350)] 18 | SLIDE_POSITION = ["top", "bottom"] 19 | WHITE_RGB = (255, 255, 255) 20 | TRANSPARENT_RGBA = (0, 0, 0, 0) 21 | BLACK_RGB = (0, 0, 0) 22 | DEFAULT_VIDEO_FPS = 24 23 | 24 | 25 | def generate_slide_images_for_all_sentences(sentences): 26 | for key, sentence in enumerate(sentences): 27 | print("Generating slide {}...".format(key)) 28 | generate_slide_images_for_sentence(key, sentence) 29 | create_youtube_thumbnail(0, YOUTUBE_THUMBNAIL_PATH) 30 | 31 | 32 | def generate_slide_images_for_sentence(sentence_key, sentence): 33 | original_image = Image.open("{}{}".format(sentence_key, ORIGINAL_IMAGE_SUFFIX)).convert(DEFAULT_IMAGE_MODE) 34 | output_path = "{}{}".format(sentence_key, CONVERTED_IMAGE_SUFFIX) 35 | resize_image(original_image, sentence_key, output_path) 36 | merge_images(original_image, output_path) 37 | create_sentence_image(sentence_key, sentence.text, "{}{}{}".format(sentence_key, SENTENCE_IMAGE_TAG, 38 | CONVERTED_IMAGE_SUFFIX)) 39 | 40 | 41 | def resize_image(image, sentence_key, output_path): 42 | try: 43 | resized_image = image.resize((DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT)) 44 | resized_image.filter(ImageFilter.GaussianBlur(30)).save(output_path) 45 | except: 46 | print("Error resizing image {}".format(sentence_key)) 47 | 48 | 49 | def merge_images(original_image, output_path): 50 | image_width, image_height = original_image.size 51 | resize_width, resize_height = [int(image_width * (DEFAULT_IMAGE_HEIGHT / image_height)), DEFAULT_IMAGE_HEIGHT] if image_height > image_width else [DEFAULT_IMAGE_WIDTH, int(image_height * (DEFAULT_IMAGE_WIDTH / image_width))] 52 | original_image = original_image.resize((resize_width, resize_height)) 53 | background_image = Image.open(output_path).convert(DEFAULT_IMAGE_MODE) 54 | background_image_width, background_image_height = background_image.size 55 | 56 | offset = ((background_image_width - resize_width) // 2, (background_image_height - resize_height) // 2) 57 | 58 | background_image.paste(original_image, offset, original_image) 59 | background_image.save(output_path) 60 | 61 | 62 | def get_text_position_by_sentence_key(sentence_key): 63 | return TEXT_POSITION[0] if sentence_key % 2 == 0 else TEXT_POSITION[1] 64 | 65 | 66 | def create_sentence_image(sentence_key, sentence_text, output_image_path): 67 | try: 68 | new_image = Image.new(DEFAULT_IMAGE_MODE, (DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT), TRANSPARENT_RGBA) 69 | text_lines = textwrap.wrap(sentence_text, width=DEFAULT_TEXT_WIDTH) 70 | draw = ImageDraw.Draw(new_image) 71 | font = ImageFont.truetype(DEFAULT_FONT_TYPE, DEFAULT_FONT_SIZE) 72 | text_position_x, text_position_y = get_text_position_by_sentence_key(sentence_key) 73 | 74 | for line in text_lines: 75 | font_width, font_height = font.getsize(line) 76 | draw.text((text_position_x + DEFAULT_SHADOW_OFFSET, text_position_y + DEFAULT_SHADOW_OFFSET), line, 77 | BLACK_RGB, 78 | font=font) 79 | draw.text((text_position_x - DEFAULT_SHADOW_OFFSET, text_position_y - DEFAULT_SHADOW_OFFSET), line, 80 | BLACK_RGB, 81 | font=font) 82 | draw.text((text_position_x + DEFAULT_SHADOW_OFFSET, text_position_y - DEFAULT_SHADOW_OFFSET), line, 83 | BLACK_RGB, 84 | font=font) 85 | draw.text((text_position_x - DEFAULT_SHADOW_OFFSET, text_position_y + DEFAULT_SHADOW_OFFSET), line, 86 | BLACK_RGB, 87 | font=font) 88 | draw.text((text_position_x, text_position_y), line, WHITE_RGB, font=font) 89 | text_position_y += font_height 90 | new_image.save(output_image_path) 91 | except: 92 | print("Couldn't add text to image {}_converted.png".format(sentence_key)) 93 | 94 | 95 | def create_youtube_thumbnail(sentence_key, output_path): 96 | original_image = Image.open("{}{}".format(sentence_key, CONVERTED_IMAGE_SUFFIX)).convert(YOUTUBE_THUMBNAIL_MODE) 97 | original_image.save(output_path) 98 | 99 | 100 | def get_slide_position_by_sentence_key(sentence_key): 101 | return SLIDE_POSITION[0] if sentence_key % 2 == 0 else SLIDE_POSITION[1] 102 | 103 | 104 | def render_video(sentences, output_path, audio_path): 105 | print("Rendering video...") 106 | image_slides = [] 107 | for key, sentence in enumerate(sentences): 108 | image_slide = ImageClip("{}{}".format(key, CONVERTED_IMAGE_SUFFIX)).set_duration(10) 109 | text_slide = ImageClip("{}{}{}".format(key, SENTENCE_IMAGE_TAG, CONVERTED_IMAGE_SUFFIX)).set_duration(10) 110 | slided_slide = text_slide.fx(transfx.slide_in, 1, get_slide_position_by_sentence_key(key)) 111 | slides_video = CompositeVideoClip([image_slide, slided_slide]) 112 | image_slides.append(slides_video) 113 | 114 | final_video = concatenate(image_slides) 115 | final_video.write_videofile(output_path, audio=audio_path, fps=DEFAULT_VIDEO_FPS) 116 | -------------------------------------------------------------------------------- /services/Youtube.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import os 3 | import random 4 | import sys 5 | import time 6 | 7 | import httplib2 8 | from googleapiclient.discovery import build 9 | from googleapiclient.errors import HttpError 10 | from googleapiclient.http import MediaFileUpload 11 | from oauth2client.client import flow_from_clientsecrets 12 | from oauth2client.file import Storage 13 | from oauth2client.tools import argparser, run_flow 14 | 15 | # Explicitly tell the underlying HTTP transport library not to retry, since 16 | # we are handling retry logic ourselves. 17 | import Content 18 | 19 | httplib2.RETRIES = 1 20 | 21 | # Maximum number of times to retry before giving up. 22 | MAX_RETRIES = 10 23 | 24 | # Always retry when these exceptions are raised. 25 | RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError) 26 | 27 | # Always retry when an apiclient.errors.HttpError with one of these status 28 | # codes is raised. 29 | RETRIABLE_STATUS_CODES = [500, 502, 503, 504] 30 | 31 | # The CLIENT_SECRETS_FILE variable specifies the name of a file that contains 32 | # the OAuth 2.0 information for this application, including its client_id and 33 | # client_secret. You can acquire an OAuth 2.0 client ID and client secret from 34 | # the Google Developers Console at 35 | # https://console.developers.google.com/. 36 | # Please ensure that you have enabled the YouTube Data API for your project. 37 | # For more information about using OAuth2 to access the YouTube Data API, see: 38 | # https://developers.google.com/youtube/v3/guides/authentication 39 | # For more information about the client_secrets.json file format, see: 40 | # https://developers.google.com/api-client-library/python/guide/aaa_client_secrets 41 | CLIENT_SECRETS_FILE = "client_secrets.json" 42 | 43 | # This OAuth 2.0 access scope allows an application to upload files to the 44 | # authenticated user's YouTube channel, but doesn't allow other types of access. 45 | YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload" 46 | YOUTUBE_API_SERVICE_NAME = "youtube" 47 | YOUTUBE_API_VERSION = "v3" 48 | 49 | # This variable defines a message to display if the CLIENT_SECRETS_FILE is 50 | # missing. 51 | MISSING_CLIENT_SECRETS_MESSAGE = """ 52 | WARNING: Please configure OAuth 2.0 53 | 54 | To make this sample run you will need to populate the client_secrets.json file 55 | found at: 56 | 57 | %s 58 | 59 | with information from the Developers Console 60 | https://console.developers.google.com/ 61 | 62 | For more information about the client_secrets.json file format, please visit: 63 | https://developers.google.com/api-client-library/python/guide/aaa_client_secrets 64 | """ % os.path.abspath(os.path.join(os.path.dirname(__file__), 65 | CLIENT_SECRETS_FILE)) 66 | 67 | VALID_PRIVACY_STATUSES = ("public", "private", "unlisted") 68 | 69 | 70 | def get_authenticated_service(): 71 | flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE, 72 | scope=YOUTUBE_UPLOAD_SCOPE, 73 | message=MISSING_CLIENT_SECRETS_MESSAGE) 74 | 75 | storage = Storage("%s-oauth2.json" % sys.argv[0]) 76 | credentials = storage.get() 77 | 78 | if credentials is None or credentials.invalid: 79 | credentials = run_flow(flow, storage) 80 | 81 | return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, 82 | http=credentials.authorize(httplib2.Http())) 83 | 84 | 85 | def initialize_upload(youtube, options): 86 | tags = None 87 | if options.keywords: 88 | tags = options.keywords.split(",") 89 | 90 | body = dict( 91 | snippet=dict( 92 | title=options.title, 93 | description=options.description, 94 | tags=tags, 95 | categoryId=options.category 96 | ), 97 | status=dict( 98 | privacyStatus=options.privacyStatus 99 | ) 100 | ) 101 | 102 | # Call the API's videos.insert method to create and upload the video. 103 | insert_request = youtube.videos().insert( 104 | part=",".join(body.keys()), 105 | body=body, 106 | # The chunksize parameter specifies the size of each chunk of data, in 107 | # bytes, that will be uploaded at a time. Set a higher value for 108 | # reliable connections as fewer chunks lead to faster uploads. Set a lower 109 | # value for better recovery on less reliable connections. 110 | # 111 | # Setting "chunksize" equal to -1 in the code below means that the entire 112 | # file will be uploaded in a single HTTP request. (If the upload fails, 113 | # it will still be retried where it left off.) This is usually a best 114 | # practice, but if you're using Python older than 2.6 or if you're 115 | # running on App Engine, you should set the chunksize to something like 116 | # 1024 * 1024 (1 megabyte). 117 | media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True) 118 | ) 119 | 120 | resumable_upload(insert_request) 121 | 122 | 123 | # This method implements an exponential backoff strategy to resume a 124 | # failed upload. 125 | def resumable_upload(insert_request): 126 | response = None 127 | error = None 128 | retry = 0 129 | while response is None: 130 | try: 131 | print("Uploading file...") 132 | status, response = insert_request.next_chunk() 133 | if 'id' in response: 134 | print("Video id '%s' was successfully uploaded." % response['id']) 135 | else: 136 | exit("The upload failed with an unexpected response: %s" % response) 137 | except HttpError as e: 138 | if e.resp.status in RETRIABLE_STATUS_CODES: 139 | error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status, 140 | e.content) 141 | else: 142 | raise 143 | except RETRIABLE_EXCEPTIONS as e: 144 | error = "A retriable error occurred: %s" % e 145 | 146 | if error is not None: 147 | print(error) 148 | retry += 1 149 | if retry > MAX_RETRIES: 150 | exit("No longer attempting to retry.") 151 | 152 | max_sleep = 2 ** retry 153 | sleep_seconds = random.random() * max_sleep 154 | print("Sleeping %f seconds and then retrying..." % sleep_seconds) 155 | time.sleep(sleep_seconds) 156 | 157 | 158 | def create_description_string(content: Content): 159 | description = "" 160 | for sentence in content.sentences: 161 | description = "{}{}\n\n".format(description, sentence.text) 162 | return description 163 | 164 | 165 | def create_keywords_string(content: Content): 166 | keywords = "" 167 | for sentence in content.sentences: 168 | keywords = "{},{}".format(keywords, sentence.keywords[0]) 169 | return keywords 170 | 171 | 172 | def upload_video(file, content: Content): 173 | options = { 174 | "file": file, 175 | "title": "{} {}".format(content.prefix, content.search_term), 176 | "description": create_description_string(content), 177 | "category": 22, 178 | "keywords": create_keywords_string(content), 179 | "privacyStatus": VALID_PRIVACY_STATUSES[2] 180 | } 181 | options = type('lamdbaobject', (object,), options)() 182 | 183 | if not os.path.exists(file): 184 | exit("File not found") 185 | 186 | youtube = get_authenticated_service() 187 | try: 188 | initialize_upload(youtube, options) 189 | except HttpError as e: 190 | print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content)) 191 | -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelTI/video-maker-bot-python/20b0c90251d582b0526d8b94a9a65f2991b0713a/services/__init__.py -------------------------------------------------------------------------------- /video-maker.py: -------------------------------------------------------------------------------- 1 | from Content import Content 2 | from services import UserInput, TextProcessing, ImageProcessing, VideoProcessing, Youtube 3 | 4 | print("Starting orchestrator") 5 | 6 | DEFAULT_CONTENT_PATH = "content.pickle" 7 | DEFAULT_VIDEO_PATH = "test.mp4" 8 | DEFAULT_AUDIO_PATH = "audio.mp3" 9 | content = Content() 10 | 11 | content.search_term, content.prefix = UserInput.get_topic_and_prefix_from_input() 12 | content.save(DEFAULT_CONTENT_PATH) 13 | 14 | content.source_content, content.source_content_sanitized = TextProcessing. \ 15 | fetch_content_and_sanitize(content.prefix, content.search_term) 16 | content.sentences = TextProcessing.fetch_keywords_from_content(content.source_content_sanitized) 17 | content.save(DEFAULT_CONTENT_PATH) 18 | 19 | content.sentences, content.downloaded_images = ImageProcessing. \ 20 | fetch_images_and_download_for_sentences(content.sentences, content.search_term, content.downloaded_images) 21 | content.save(DEFAULT_CONTENT_PATH) 22 | 23 | VideoProcessing.generate_slide_images_for_all_sentences(content.sentences) 24 | VideoProcessing.render_video(content.sentences, DEFAULT_VIDEO_PATH, DEFAULT_AUDIO_PATH) 25 | 26 | Youtube.upload_video(DEFAULT_VIDEO_PATH, content) 27 | 28 | print("Start sharing your video :)") 29 | --------------------------------------------------------------------------------