wower99 commited on
Commit
b3081c2
·
1 Parent(s): 32753c7

youtube-short-generator:v1

Browse files
.gitignore ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ service_account_file.json
3
+ venv/
4
+
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ # db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .venv
128
+ env/
129
+ venv/
130
+ ENV/
131
+ env.bak/
132
+ venv.bak/
133
+
134
+ # Spyder project settings
135
+ .spyderproject
136
+ .spyproject
137
+
138
+ # Rope project settings
139
+ .ropeproject
140
+
141
+ # mkdocs documentation
142
+ /site
143
+
144
+ # mypy
145
+ .mypy_cache/
146
+ .dmypy.json
147
+ dmypy.json
148
+
149
+ # Pyre type checker
150
+ .pyre/
151
+
152
+ # pytype static type analyzer
153
+ .pytype/
154
+
155
+ # Cython debug symbols
156
+ cython_debug/
157
+
158
+ # PyCharm
159
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
162
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163
+ #.idea/
app.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import re
4
+ from youtube_short_generator import YoutubeShortGenerator
5
+
6
+ def main():
7
+ st.title("YouTube Shorts Generator 🎥")
8
+ st.write("Generate AI-powered YouTube Shorts by just entering a title!")
9
+
10
+ # User Input
11
+ video_title = st.text_input("Enter Video Title:", "Top 3 Marvel Superheroes")
12
+
13
+ if st.button("Generate Video"):
14
+ if video_title.strip():
15
+ # Validate that the title starts with "Top 3" or "Top 5" (case-insensitive)
16
+ pattern = r"^top\s*(3|5)\b"
17
+ if not re.match(pattern, video_title.strip(), re.IGNORECASE):
18
+ st.warning("Please start the title with 'Top 3' or 'Top 5'.")
19
+ return
20
+
21
+ st.info("Starting video generation process...")
22
+
23
+ try:
24
+ # Initialize Generator
25
+ yt_generator = YoutubeShortGenerator()
26
+
27
+ with st.spinner("Analyzing title and extracting keywords..."):
28
+ yt_generator.title_to_keywords(video_title)
29
+ st.info("Keywords extracted successfully!")
30
+
31
+ with st.spinner("Generating images..."):
32
+ yt_generator.generate_images()
33
+ st.info("Images generated successfully!")
34
+
35
+ with st.spinner("Overlaying text on images..."):
36
+ yt_generator.overlay_text_to_images()
37
+ st.info("Text overlay completed!")
38
+
39
+ with st.spinner("Generating audio clips..."):
40
+ yt_generator.generate_audio_clips()
41
+ st.info("Audio clips generated successfully!")
42
+
43
+ with st.spinner("Combining images and audio into video..."):
44
+ yt_generator.make_video()
45
+ st.info("Video is being finalized!")
46
+
47
+ # Get the generated video path
48
+ video_path = os.path.join(yt_generator.generated_video_dir, 'final_video.mp4')
49
+
50
+ if os.path.exists(video_path):
51
+ st.success("Video generated successfully!")
52
+
53
+ # Provide a download link
54
+ with open(video_path, "rb") as file:
55
+ st.download_button(
56
+ label="Download Video",
57
+ data=file,
58
+ file_name="youtube_short.mp4",
59
+ mime="video/mp4"
60
+ )
61
+ else:
62
+ st.error("Error: Video file not found.")
63
+ except Exception as e:
64
+ st.error(f"An error occurred: {e}")
65
+ else:
66
+ st.warning("Please enter a valid title.")
67
+
68
+ if __name__ == "__main__":
69
+ main()
constants.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ # API keys need to be configured in .env
7
+ llm_api_keys = {
8
+ "openai": os.getenv('OPENAI_API_KEY'),
9
+ "groq": os.getenv('GROQ_API_KEY'),
10
+ "anthropic": os.getenv('ANTHROPIC_API_KEY')
11
+ }
12
+
13
+ # optionally change providers
14
+ CHOSEN_LLM_PROVIDER = os.getenv('CHOSEN_LLM_PROVIDER','groq')
15
+ CHOSEN_EMBEDDING_PROVIDER = os.getenv('CHOSEN_EMBEDDING_PROVIDER','ollama')
16
+
17
+
18
+ FONT_BASE_DIR = 'fonts'
19
+ HF_TOKEN = os.getenv("HF_TOKEN", None)
20
+ IMAGE_GENERATION_SPACE_NAME="habib926653/stabilityai-stable-diffusion-3.5-large-turbo"
21
+
22
+
23
+ # LLM Models dictionary
24
+ selected_llm_model = {
25
+ "openai": os.getenv('GPT_MODEL','gpt-4o-mini'),
26
+ "groq": os.getenv('GROQ_MODEL','llama-3.3-70b-versatile'),
27
+ "anthropic": os.getenv('ANTHROPIC_MODEL','claude-3-5-sonnet-latest'),
28
+ "ollama": os.getenv('OLLAMA_MODEL','llama3.1')
29
+
30
+ }
31
+
32
+
33
+ # Embedding Model Models dictionary
34
+ selected_embedding_model = {
35
+ "openai": os.getenv('GPT_EMBEDDING_MODEL'),
36
+ "groq": os.getenv('GROQ_EMBEDDING_MODEL'),
37
+ "anthropic": os.getenv('ANTHROPIC_EMBEDDING_MODEL'),
38
+ "ollama": os.getenv('OLLAMA_EMBEDDING_MODEL')
39
+
40
+ }
env.sample ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ GROQ_API_KEY=GROQ_API_KEY
2
+ HF_TOKEN=HF_TOKEN
fonts/Roboto/LICENSE.txt ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
fonts/Roboto/Roboto-Black.ttf ADDED
Binary file (168 kB). View file
 
fonts/Roboto/Roboto-BlackItalic.ttf ADDED
Binary file (174 kB). View file
 
fonts/Roboto/Roboto-Bold.ttf ADDED
Binary file (167 kB). View file
 
fonts/Roboto/Roboto-BoldItalic.ttf ADDED
Binary file (172 kB). View file
 
fonts/Roboto/Roboto-Italic.ttf ADDED
Binary file (171 kB). View file
 
fonts/Roboto/Roboto-Light.ttf ADDED
Binary file (167 kB). View file
 
fonts/Roboto/Roboto-LightItalic.ttf ADDED
Binary file (173 kB). View file
 
fonts/Roboto/Roboto-Medium.ttf ADDED
Binary file (169 kB). View file
 
fonts/Roboto/Roboto-MediumItalic.ttf ADDED
Binary file (173 kB). View file
 
fonts/Roboto/Roboto-Regular.ttf ADDED
Binary file (168 kB). View file
 
fonts/Roboto/Roboto-Thin.ttf ADDED
Binary file (168 kB). View file
 
fonts/Roboto/Roboto-ThinItalic.ttf ADDED
Binary file (173 kB). View file
 
fonts/Weaselic.ttf ADDED
Binary file (19.1 kB). View file
 
function_wrap_center.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image, ImageDraw, ImageFont,ImageStat,ImageFilter,ImageEnhance
2
+ from django.conf import settings
3
+ import constants
4
+ import os
5
+
6
+ def wrap_text(draw, text, font, max_width):
7
+ words = text.split()
8
+ lines = []
9
+ current_line = words[0]
10
+
11
+ for word in words[1:]:
12
+ test_line = current_line + " " + word
13
+ if draw.textlength(test_line, font) <= max_width:
14
+ current_line = test_line
15
+ else:
16
+ lines.append(current_line)
17
+ current_line = word
18
+
19
+ lines.append(current_line)
20
+ return lines
21
+
22
+ def draw_text_centered(draw, lines, position, font, max_width, padding, fill='yellow'):
23
+ y = position[1]
24
+
25
+ for line in lines:
26
+ text_width = draw.textlength(line, font)
27
+ x = position[0] + (max_width - text_width) // 2
28
+ draw.text((x, y), line, font=font, fill=fill)
29
+ y += font.getsize('hg')[1] + padding
30
+
31
+ def get_wrapped_text_size(draw, lines, font, padding):
32
+ line_height = font.getsize('hg')[1]
33
+ total_height = len(lines) * (line_height + padding) - padding
34
+ max_line_width = max(draw.textlength(line, font) for line in lines)
35
+ return total_height, max_line_width
36
+
37
+ def dynamically_adjust_font(draw, text, font, max_width, max_height, padding):
38
+ lines = wrap_text(draw, text, font, max_width)
39
+ total_height, _ = get_wrapped_text_size(draw, lines, font, padding)
40
+ while total_height > max_height and font.size > 10:
41
+ font = ImageFont.truetype(font.path, font.size - 1)
42
+ lines = wrap_text(draw, text, font, max_width)
43
+ total_height, _ = get_wrapped_text_size(draw, lines, font, padding)
44
+ return font, lines
45
+
46
+
47
+ def is_image_dark(image,threshold=128):
48
+ """Determine if the image is predominantly dark, light or in the middle and return a suitable color for overlay"""
49
+ grayscale = image.convert('L') # Convert to grayscale
50
+ stat = ImageStat.Stat(grayscale)
51
+ avg_brightness = stat.mean[0]
52
+
53
+ if avg_brightness < threshold:
54
+ return ('dark','yellow')
55
+ else:
56
+ return ('light','red')
57
+
58
+
59
+ def add_text_to_image(image_path, text, is_title=True, save_to=None):
60
+ # Load the image
61
+ image = Image.open(image_path)
62
+
63
+ # Resize the image
64
+ resized_image = image.resize((360, 740))
65
+
66
+ # Get resized image dimensions
67
+ image_width, image_height = resized_image.size
68
+
69
+ # check if the image is more darker or more lighter
70
+ image_brightness_level = is_image_dark(resized_image)
71
+
72
+ # Create a drawing object
73
+ draw = ImageDraw.Draw(resized_image)
74
+
75
+ # setting up font_paths
76
+ font_paths = {
77
+ "weaselic" : os.path.join(constants.FONT_BASE_DIR,'Weaselic.ttf'),
78
+ "black": os.path.join(constants.FONT_BASE_DIR,'Roboto/Roboto-Black.ttf'),
79
+ "bold": os.path.join(constants.FONT_BASE_DIR,'Roboto/Roboto-Bold.ttf'),
80
+ "medium": os.path.join(constants.FONT_BASE_DIR,'Roboto/Roboto-Medium.ttf'),
81
+ "light": os.path.join(constants.FONT_BASE_DIR,'Roboto/Roboto-Light.ttf'),
82
+ "thin": os.path.join(constants.FONT_BASE_DIR,'Roboto/Roboto-Thin.ttf')
83
+ }
84
+
85
+ # Define fonts
86
+ fonts = {
87
+ "weaselic" : ImageFont.truetype(font_paths['weaselic'], 43),
88
+ "black": ImageFont.truetype(font_paths['black'], 40),
89
+ "bold": ImageFont.truetype(font_paths['bold'], 35),
90
+ "medium": ImageFont.truetype(font_paths['medium'], 40),
91
+ "light": ImageFont.truetype(font_paths['light'], 30),
92
+ "thin": ImageFont.truetype(font_paths['thin'], 30)
93
+ }
94
+
95
+ padding = 5
96
+ margin_between = 50 # Margin between title and description
97
+ safe_margin = 10 # Margin from the image edges
98
+
99
+ if is_title:
100
+ font = fonts["bold"]
101
+ max_width = image_width - 2 * safe_margin
102
+ max_height = (image_height - 2 * safe_margin) // 2
103
+
104
+ # Dynamically adjust font size and wrap text
105
+ font, lines = dynamically_adjust_font(draw, text, font, max_width, max_height, padding)
106
+
107
+ # Calculate total text height
108
+ total_height, _ = get_wrapped_text_size(draw, lines, font, padding)
109
+
110
+ # Positioning
111
+ if total_height > image_height - 2 * safe_margin:
112
+ print("Text does not fit within the image boundaries.")
113
+ else:
114
+ position = (safe_margin, safe_margin + (image_height - total_height) // 2 - 120)
115
+
116
+ # Draw the rectangle behind the text
117
+ rect_x0 = safe_margin
118
+ rect_x1 = image_width - safe_margin
119
+ rect_y0 = position[1] - padding
120
+ rect_y1 = rect_y0 + total_height + 2 * padding
121
+ draw.rectangle([(rect_x0, rect_y0), (rect_x1, rect_y1)], fill="yellow")
122
+
123
+ # Draw wrapped and centered text
124
+ draw_text_centered(draw, lines, (safe_margin, rect_y0 + padding), font, rect_x1 - rect_x0, padding, fill='black')
125
+
126
+ else:
127
+ font = fonts["weaselic"]
128
+ max_width = image_width - 2 * safe_margin
129
+ max_height = (image_height - 2 * safe_margin) // 2
130
+
131
+ # Dynamically adjust font size and wrap text
132
+ font, lines = dynamically_adjust_font(draw, text, font, max_width, max_height, padding)
133
+
134
+ # Calculate total text height
135
+ total_height, _ = get_wrapped_text_size(draw, lines, font, padding)
136
+
137
+ # Positioning
138
+ if total_height > image_height - 2 * safe_margin:
139
+ print("Text does not fit within the image boundaries.")
140
+ else:
141
+ position = (safe_margin, safe_margin + (image_height - total_height) // 2)
142
+
143
+ # Draw wrapped and centered text
144
+ description_color = image_brightness_level[1]
145
+ draw_text_centered(draw, lines, position, font, max_width, padding,fill = description_color)
146
+
147
+ # Save the image if save_to is provided
148
+ if save_to:
149
+ resized_image.save(save_to)
150
+
151
+ # Example usage:
152
+ if __name__ == '__main__':
153
+ title_text = "Top 5 mountains in the world"
154
+ description_text = "Mount Everest"
155
+
156
+ add_text_to_image('wow.png', title_text, is_title=True, save_to='output_title.png')
157
+ add_text_to_image('wow.png', description_text, is_title=False, save_to='output_description.png')
image_generator.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pollinations
2
+ import constants
3
+ from PIL import Image
4
+ from gradio_client import Client
5
+
6
+
7
+ class ImageGenerator:
8
+ def __init__(self, model=pollinations.Image.flux(), seed="random", width=720, height=1280, enhance=False, nologo=True, private=True, safe=False, referrer="pollinations.py"):
9
+ # Initialize the image model with provided parameters
10
+ self.image_model = pollinations.Image(
11
+ model=model,
12
+ seed=seed,
13
+ width=width,
14
+ height=height,
15
+ enhance=False,
16
+ nologo=nologo,
17
+ private=private,
18
+ safe=safe,
19
+ referrer=referrer
20
+ )
21
+
22
+ def generate_image_with_pollinations_ai(self, prompt):
23
+ # Generate image using the provided prompt
24
+ try:
25
+ image = self.image_model(prompt=prompt)
26
+ return image # Return the generated image object
27
+ except Exception as e:
28
+ print(f"Error generating image: {e}")
29
+ return None # Return None if there's an error
30
+
31
+
32
+ def generate_image(self, prompt, path='test_image.png'):
33
+ try:
34
+ # Initialize the Gradio Client with Hugging Face token
35
+ client = Client(constants.IMAGE_GENERATION_SPACE_NAME, hf_token=constants.HF_TOKEN)
36
+
37
+ # Make the API request
38
+ result = client.predict(
39
+ param_0=prompt, # Text prompt for image generation
40
+ api_name="/predict"
41
+ )
42
+
43
+ image = Image.open(result)
44
+ image.save(path)
45
+
46
+ # Return the result (which includes the URL or file path)
47
+ return result
48
+
49
+ except Exception as e:
50
+ print(f"Error during image generation: {e}")
51
+ return {"error": str(e)}
52
+
53
+
54
+ # Example usage
55
+ if __name__ == '__main__':
56
+ image_generator = ImageGenerator() # You can pass custom params here if needed
57
+ result = image_generator.generate_image("A cat with flowers around it.",path='wow9.png')
58
+
59
+ print(result)
structured_output.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Type, Optional
2
+ from pydantic import BaseModel, Field
3
+ from langgraph.graph import StateGraph, START, END
4
+ import constants
5
+ from typing import TypedDict
6
+
7
+
8
+
9
+ # Define the State structure (similar to previous definition)
10
+ class State(TypedDict):
11
+ messages: list
12
+ output: Optional[BaseModel]
13
+
14
+
15
+ # Generic Pydantic model-based structured output extractor
16
+ class StructuredOutputExtractor:
17
+ def __init__(self, response_schema: Type[BaseModel]):
18
+ """
19
+ Initializes the extractor for any given structured output model.
20
+
21
+ :param response_schema: Pydantic model class used for structured output extraction
22
+ """
23
+ self.response_schema = response_schema
24
+
25
+ # Initialize language model (provider and API keys come from constants.py)
26
+ self.llm = self._choose_llm_provider(constants.CHOSEN_LLM_PROVIDER)
27
+
28
+ # Bind the model with structured output capability
29
+ self.structured_llm = self.llm.with_structured_output(response_schema)
30
+
31
+ # Build the graph for structured output
32
+ self._build_graph()
33
+
34
+ def _build_graph(self):
35
+ """
36
+ Build the LangGraph computational graph for structured extraction.
37
+ """
38
+ graph_builder = StateGraph(State)
39
+
40
+ # Add nodes and edges for structured output
41
+ graph_builder.add_node("extract", self._extract_structured_info)
42
+ graph_builder.add_edge(START, "extract")
43
+ graph_builder.add_edge("extract", END)
44
+
45
+ self.graph = graph_builder.compile()
46
+
47
+ def _extract_structured_info(self, state: dict):
48
+ """
49
+ Extract structured information using the specified response model.
50
+
51
+ :param state: Current graph state
52
+ :return: Updated state with structured output
53
+ """
54
+ query = state['messages'][-1].content
55
+ print(f"Processing query: {query}")
56
+ try:
57
+ # Extract details using the structured model
58
+ output = self.structured_llm.invoke(query)
59
+ # Return the structured response
60
+ return {"output": output}
61
+ except Exception as e:
62
+ print(f"Error during extraction: {e}")
63
+ return {"output": None}
64
+
65
+ def extract(self, query: str) -> Optional[BaseModel]:
66
+ """
67
+ Public method to extract structured information.
68
+
69
+ :param query: Input query for structured output extraction
70
+ :return: Structured model object or None
71
+ """
72
+ from langchain_core.messages import HumanMessage
73
+
74
+ result = self.graph.invoke({
75
+ "messages": [HumanMessage(content=query)]
76
+ })
77
+ # Return the structured model response, if available
78
+ result = result.get('output')
79
+ return result
80
+
81
+ def _choose_llm_provider(self, chosen_llm_provider):
82
+ """Dynamically imports and selects the LLM provider based on configuration, and asks to install the library if it's missing."""
83
+ api_key = constants.llm_api_keys.get(chosen_llm_provider)
84
+ if chosen_llm_provider == 'openai':
85
+ from langchain_openai import ChatOpenAI
86
+ return ChatOpenAI(model=constants.selected_llm_model.get('openai'), streaming=True, api_key=api_key)
87
+ elif chosen_llm_provider == 'ollama':
88
+ from langchain_ollama import ChatOllama
89
+ return ChatOllama(model=constants.selected_llm_model.get('ollama')) # streaming is enabled by default
90
+ elif chosen_llm_provider == 'groq':
91
+ from langchain_groq import ChatGroq
92
+ return ChatGroq(model=constants.selected_llm_model.get('groq'), streaming=True, api_key=api_key)
93
+ elif chosen_llm_provider == 'anthropic':
94
+ from langchain_anthropic import ChatAnthropic
95
+ return ChatAnthropic(model=constants.selected_llm_model.get('anthropic'), streaming=True, api_key=api_key)
96
+ else:
97
+ raise ValueError(f"Unsupported LLM provider: {chosen_llm_provider}")
98
+
99
+
100
+ if __name__ == '__main__':
101
+
102
+ # Example Pydantic model (e.g., Movie)
103
+ class Movie(BaseModel):
104
+ title: str = Field(description="the title of the youtube video")
105
+ title_image: str = Field(description="highly detailed and descriptive image prompt for the Title")
106
+ items: list[str] = Field(description="top n number of requested items")
107
+ image_prompts: list[str] = Field(description="highly detailed and descriptive image prompts for each item ")
108
+
109
+
110
+
111
+ # Example usage with a generic structured extractor
112
+ extractor = StructuredOutputExtractor(response_schema=Movie)
113
+
114
+ query = "Top 5 Superheroes"
115
+
116
+ result = extractor.extract(query)
117
+ print(type(result))
118
+ if result:
119
+ print(result)
youtube_short_generator.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import shutil
4
+ from image_generator import ImageGenerator
5
+ from moviepy.editor import ImageClip, concatenate_videoclips,AudioFileClip
6
+ from function_wrap_center import add_text_to_image
7
+ from gtts import gTTS
8
+ from structured_output import StructuredOutputExtractor
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class YoutubeShortGenerator:
13
+
14
+ def __init__(self):
15
+ self.video_title = None
16
+ self.result = None
17
+ self.media_dir = 'generated_media'
18
+ self.generated_video_dir = None
19
+ self.image_dir = None
20
+ self.audio_clips_dir = None
21
+ self.video_path = None
22
+
23
+ os.makedirs(self.media_dir,exist_ok=True)
24
+
25
+ def title_to_keywords(self, title):
26
+ # Define your data extraction model here
27
+ class TopN(BaseModel):
28
+ title: str = Field(description="the title of the youtube video")
29
+ title_image_prompt: str = Field(description="highly detailed and descriptive image prompt for the background image of Title")
30
+ items: list[str] = Field(description="top n number of requested items")
31
+ items_image_prompts: list[str] = Field(description="highly detailed and descriptive image prompts for each item ")
32
+
33
+ # Assuming StructuredOutputExtractor is defined and functional
34
+ extractor = StructuredOutputExtractor(response_schema=TopN)
35
+ result = extractor.extract(title)
36
+ self.result = result
37
+
38
+ # create main directory for saving video related content i.e images, audio_clips
39
+ video_title = self.result.title
40
+ unique_id = uuid.uuid4().hex
41
+ folder_path = f"{self.media_dir}/generated_{video_title}_{unique_id}"
42
+ self.generated_video_dir = folder_path
43
+
44
+ return self
45
+
46
+ def generate_images(self):
47
+ if not self.result:
48
+ print("No data available. Call title_to_keywords first.")
49
+ return self
50
+
51
+ print(self.result,'inside generate_images()')
52
+
53
+ generator = ImageGenerator() # Your custom image generator
54
+ folder_path = f"{self.generated_video_dir}/generated_images" # Append unique ID
55
+ os.makedirs(folder_path, exist_ok=True) # Ensure directory is created
56
+ self.image_dir = folder_path
57
+
58
+ # Generate Title Image
59
+ # generator.generate_image(self.result.title_image_prompt, path=f"{folder_path}/title.png")
60
+ print("Title Prompt: ",self.result.title_image_prompt)
61
+
62
+ # generate images using pollinations_ai
63
+ # title_image = generator.generate_image(self.result.title_image_prompt)
64
+ # title_image.save(f"{folder_path}/title.png")
65
+
66
+ # generate images using stable-diffusion-turbo
67
+ generator.generate_image(self.result.title_image_prompt, path=f"{folder_path}/title.png")
68
+
69
+
70
+ print("Generating Images...")
71
+ image_prompts = self.result.items_image_prompts
72
+ print("items image prompts: ", image_prompts)
73
+ image_prompts = reversed(image_prompts)
74
+ print("Image Prompts: ", image_prompts) # Placeholder for actual image processing
75
+
76
+ # Generate and save images
77
+ for index, image_prompt in enumerate(image_prompts):
78
+
79
+ # generate images using pollinations_ai
80
+ # image = generator.generate_image(image_prompt)
81
+ # image.save(f"{folder_path}/{index}.png")
82
+
83
+ # generate images using stable-diffusion-turbo
84
+ generator.generate_image(image_prompt, f"{folder_path}/{index}.png" )
85
+ print(f"Image {index} saved.")
86
+
87
+ return self # Return self for further chaining if needed
88
+
89
+
90
+ def overlay_text_to_images(self):
91
+ if not self.result:
92
+ print("No data available. Call title_to_keywords first.")
93
+ return self
94
+
95
+ title_text = self.result.title
96
+ text_items = reversed(self.result.items)
97
+ print("text_items ",text_items)
98
+
99
+ # add text to title image
100
+ add_text_to_image(text=title_text,image_path=f"{self.image_dir}/title.png", save_to=f"{self.image_dir}/title.png")
101
+
102
+ # add text to other images
103
+ for index, text in enumerate(text_items):
104
+ image_path = f"{self.image_dir}/{index}.png"
105
+ add_text_to_image(text=text,image_path=image_path,is_title=False, save_to=image_path)
106
+
107
+ return self
108
+
109
+
110
+
111
+
112
+ def generate_audio_clips(self):
113
+ if not self.result:
114
+ print("No data available. Call title_to_keywords first.")
115
+ return self
116
+
117
+ print("Generating Title Audio...")
118
+ overlay_title = self.result.title
119
+ print("Title: ", overlay_title)
120
+
121
+
122
+ print("Generating Audio Clips...")
123
+ overlay_text_items = reversed(self.result.items)
124
+ print("Overlay Text: ", overlay_text_items) # Placeholder for actual text processing
125
+
126
+ folder_path = f"{self.generated_video_dir}/generated_audio_clips" # Append unique ID
127
+ self.audio_clips_dir = folder_path
128
+
129
+ os.makedirs(folder_path, exist_ok=True) # Ensure directory is created
130
+
131
+ # Generate title Audio
132
+ title_tts = gTTS(text=overlay_title)
133
+ title_tts.save(f"{folder_path}/title.mp3")
134
+ print(f"Title Audio clip title.mp3 saved.")
135
+
136
+ # Generate and save audio clips
137
+ for index, text_overlay in enumerate(overlay_text_items):
138
+ tts = gTTS(text=text_overlay) # Generate audio clip
139
+ tts.save(f"{folder_path}/{index}.mp3") # Save the audio as MP3
140
+ print(f"Audio clip {index} saved.")
141
+
142
+ return self # Return self for further chaining if needed
143
+
144
+
145
+ def make_video(self):
146
+ # Ensure title is included and sorted properly
147
+ audio_files = sorted(os.listdir(self.audio_clips_dir), key=lambda x: (x != "title.mp3", int(x.split(".")[0]) if x != "title.mp3" else -1))
148
+ image_files = sorted(os.listdir(self.image_dir), key=lambda x: (x != "title.png", int(x.split(".")[0]) if x != "title.png" else -1))
149
+
150
+ print("Sorted audio files:", audio_files)
151
+ print("Sorted image files:", image_files)
152
+
153
+ # Initialize audio clips
154
+ audio_clips = [AudioFileClip(os.path.join(self.audio_clips_dir, audio)) for audio in audio_files]
155
+
156
+ # Initialize image clips with matching durations
157
+ image_clips = [ImageClip(os.path.join(self.image_dir, image)).set_duration(audio.duration)
158
+ for image, audio in zip(image_files, audio_clips)]
159
+
160
+ # Attach audio to images
161
+ image_clips_with_audio = [image.set_audio(audio) for image, audio in zip(image_clips, audio_clips)]
162
+
163
+ # Concatenate all video clips
164
+ video_clip = concatenate_videoclips(image_clips_with_audio, method="compose")
165
+
166
+ # Save the final video
167
+ video_clip.write_videofile(
168
+ os.path.join(self.generated_video_dir, 'final_video.mp4'),
169
+ codec='libx264',
170
+ fps=24
171
+ )
172
+
173
+ self.remove_directory(self.image_dir)
174
+ self.remove_directory(self.audio_clips_dir)
175
+
176
+
177
+ existing_videos = sorted(
178
+ [os.path.join(self.media_dir, d) for d in os.listdir(self.media_dir)
179
+ if os.path.isdir(os.path.join(self.media_dir, d))],
180
+ key=os.path.getctime # Sort by creation time (oldest first)
181
+ )
182
+
183
+ if len(existing_videos) > 5:
184
+ for old_video_dir in existing_videos[:-5]: # Keep last 5, delete the rest
185
+ if old_video_dir != self.generated_video_dir: # Ensure we don't delete the current video
186
+ self.remove_directory(old_video_dir)
187
+
188
+ @staticmethod
189
+ def remove_directory(dir_path):
190
+ """
191
+ Remove the specified directory and all its contents.
192
+ """
193
+ if os.path.isdir(dir_path):
194
+ shutil.rmtree(dir_path)
195
+ print(f"{dir_path} and its contents have been removed.")
196
+ else:
197
+ print(f"{dir_path} does not exist or is not a directory.")
198
+
199
+
200
+
201
+
202
+
203
+ if __name__ == '__main__':
204
+ yt_short_generator = YoutubeShortGenerator()
205
+ result = yt_short_generator.title_to_keywords("top 3 Marvel Superheroes").generate_images().overlay_text_to_images().generate_audio_clips().make_video()
206
+
207
+
208
+