prolixab
commited on
Commit
·
ad31616
0
Parent(s):
Initial commit
Browse files- .gitattributes +2 -0
- .gitignore +157 -0
- .idea/.gitignore +3 -0
- .idea/batcher.iml +10 -0
- .idea/inspectionProfiles/profiles_settings.xml +6 -0
- .idea/misc.xml +10 -0
- .idea/modules.xml +8 -0
- .idea/vcs.xml +6 -0
- LICENSE +21 -0
- README.md +1 -0
- client_secrets.json +9 -0
- demux/gradio-gradio.txt +0 -0
- demux/utbildning-dystopic.txt +5 -0
- demux/utbildning-utopic.txt +0 -0
- ffmpegmaker.py +91 -0
- filmr.py +87 -0
- gradio_run.py +21 -0
- image.py +137 -0
- prompts.py +17 -0
- requirements.txt +11 -0
- script.py +32 -0
- script/utbildning-dystopi.txt +29 -0
- script/utbildning-dystopic.txt +29 -0
- speech.py +24 -0
- youtube.py +181 -0
.gitattributes
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
# Auto detect text files and perform LF normalization
|
2 |
+
* text=auto
|
.gitignore
ADDED
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py,cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# poetry
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
102 |
+
#poetry.lock
|
103 |
+
|
104 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
105 |
+
__pypackages__/
|
106 |
+
|
107 |
+
# Celery stuff
|
108 |
+
celerybeat-schedule
|
109 |
+
celerybeat.pid
|
110 |
+
|
111 |
+
# SageMath parsed files
|
112 |
+
*.sage.py
|
113 |
+
|
114 |
+
# Environments
|
115 |
+
.env
|
116 |
+
.venv
|
117 |
+
env/
|
118 |
+
venv/
|
119 |
+
ENV/
|
120 |
+
env.bak/
|
121 |
+
venv.bak/
|
122 |
+
|
123 |
+
# Spyder project settings
|
124 |
+
.spyderproject
|
125 |
+
.spyproject
|
126 |
+
|
127 |
+
# Rope project settings
|
128 |
+
.ropeproject
|
129 |
+
|
130 |
+
# mkdocs documentation
|
131 |
+
/site
|
132 |
+
|
133 |
+
# mypy
|
134 |
+
.mypy_cache/
|
135 |
+
.dmypy.json
|
136 |
+
dmypy.json
|
137 |
+
|
138 |
+
# Pyre type checker
|
139 |
+
.pyre/
|
140 |
+
|
141 |
+
# pytype static type analyzer
|
142 |
+
.pytype/
|
143 |
+
|
144 |
+
# Cython debug symbols
|
145 |
+
cython_debug/
|
146 |
+
|
147 |
+
# PyCharm
|
148 |
+
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
149 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
150 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
151 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
152 |
+
#.idea/
|
153 |
+
|
154 |
+
audio
|
155 |
+
image
|
156 |
+
music
|
157 |
+
video
|
.idea/.gitignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# Default ignored files
|
2 |
+
/shelf/
|
3 |
+
/workspace.xml
|
.idea/batcher.iml
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<module type="PYTHON_MODULE" version="4">
|
3 |
+
<component name="NewModuleRootManager">
|
4 |
+
<content url="file://$MODULE_DIR$">
|
5 |
+
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
6 |
+
</content>
|
7 |
+
<orderEntry type="jdk" jdkName="Python 3.12 (filmr)" jdkType="Python SDK" />
|
8 |
+
<orderEntry type="sourceFolder" forTests="false" />
|
9 |
+
</component>
|
10 |
+
</module>
|
.idea/inspectionProfiles/profiles_settings.xml
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<component name="InspectionProjectProfileManager">
|
2 |
+
<settings>
|
3 |
+
<option name="USE_PROJECT_PROFILE" value="false" />
|
4 |
+
<version value="1.0" />
|
5 |
+
</settings>
|
6 |
+
</component>
|
.idea/misc.xml
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<project version="4">
|
3 |
+
<component name="Black">
|
4 |
+
<option name="sdkName" value="Python 3.12 (batcher)" />
|
5 |
+
</component>
|
6 |
+
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (filmr)" project-jdk-type="Python SDK" />
|
7 |
+
<component name="PyCharmProfessionalAdvertiser">
|
8 |
+
<option name="shown" value="true" />
|
9 |
+
</component>
|
10 |
+
</project>
|
.idea/modules.xml
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<project version="4">
|
3 |
+
<component name="ProjectModuleManager">
|
4 |
+
<modules>
|
5 |
+
<module fileurl="file://$PROJECT_DIR$/.idea/batcher.iml" filepath="$PROJECT_DIR$/.idea/batcher.iml" />
|
6 |
+
</modules>
|
7 |
+
</component>
|
8 |
+
</project>
|
.idea/vcs.xml
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<project version="4">
|
3 |
+
<component name="VcsDirectoryMappings">
|
4 |
+
<mapping directory="" vcs="Git" />
|
5 |
+
</component>
|
6 |
+
</project>
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2024 prolixab
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
# Filmr
|
client_secrets.json
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"web": {
|
3 |
+
"client_id": "[[INSERT CLIENT ID HERE]]",
|
4 |
+
"client_secret": "[[INSERT CLIENT SECRET HERE]]",
|
5 |
+
"redirect_uris": [],
|
6 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
7 |
+
"token_uri": "https://accounts.google.com/o/oauth2/token"
|
8 |
+
}
|
9 |
+
}
|
demux/gradio-gradio.txt
ADDED
File without changes
|
demux/utbildning-dystopic.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
file D:\\repos\\filmr\\video\\utbildning-dystopic-0.avi
|
2 |
+
file D:\\repos\\filmr\\video\\utbildning-dystopic-1.avi
|
3 |
+
file D:\\repos\\filmr\\video\\utbildning-dystopic-2.avi
|
4 |
+
file D:\\repos\\filmr\\video\\utbildning-dystopic-3.avi
|
5 |
+
file D:\\repos\\filmr\\video\\utbildning-dystopic-4.avi
|
demux/utbildning-utopic.txt
ADDED
File without changes
|
ffmpegmaker.py
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import ffmpeg
|
2 |
+
import os
|
3 |
+
import moviepy
|
4 |
+
from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip, vfx, afx
|
5 |
+
from moviepy.video.compositing.concatenate import concatenate_videoclips
|
6 |
+
|
7 |
+
|
8 |
+
def make_film(filelist, path, filename):
|
9 |
+
output_file = os.path.join(path, filename)
|
10 |
+
ffmpeg.input(filename=filelist, f='concat', safe='0').output(output_file).run()
|
11 |
+
|
12 |
+
|
13 |
+
def make_title(intro_filmclip):
|
14 |
+
txt_title = (TextClip("15th century dancing\n(hypothetical)", fontsize=70,
|
15 |
+
font="Century-Schoolbook-Roman", color="white")
|
16 |
+
.margin(top=15, opacity=0)
|
17 |
+
.set_position(("center", "center")))
|
18 |
+
|
19 |
+
title = (CompositeVideoClip([intro_filmclip, txt_title])
|
20 |
+
.fadein(.5)
|
21 |
+
.set_duration(intro_filmclip.duration))
|
22 |
+
|
23 |
+
return title
|
24 |
+
|
25 |
+
|
26 |
+
def make_credits(title):
|
27 |
+
txt_credits = """
|
28 |
+
CREDITS
|
29 |
+
|
30 |
+
Video excerpt: Le combat en armure au XVe siècle
|
31 |
+
By J. Donzé, D. Jaquet, T. Schmuziger,
|
32 |
+
Université de Genève, Musée National de Moyen Age
|
33 |
+
|
34 |
+
Music: "Frontier", by DOCTOR VOX
|
35 |
+
Under licence Creative Commons
|
36 |
+
https://www.youtube.com/user/DOCTORVOXofficial
|
37 |
+
|
38 |
+
Video editing © Zulko 2014
|
39 |
+
Licence Creative Commons (CC BY 4.0)
|
40 |
+
Edited with MoviePy: http://zulko.github.io/moviepy/
|
41 |
+
"""
|
42 |
+
|
43 |
+
credits = (TextClip(txt_credits, color='white',
|
44 |
+
font="Century-Schoolbook-Roman", fontsize=35, kerning=-2,
|
45 |
+
interline=-1, bg_color='black', size=title.size)
|
46 |
+
.set_duration(2.5)
|
47 |
+
.fadein(.5)
|
48 |
+
.fadeout(.5))
|
49 |
+
|
50 |
+
return credits
|
51 |
+
|
52 |
+
|
53 |
+
def make_short_film(path, img, audio, filename, i):
|
54 |
+
output_file = os.path.join(path, filename)
|
55 |
+
input_img = ffmpeg.input(img)
|
56 |
+
input_audio = ffmpeg.input(audio)
|
57 |
+
final_filename = f'{output_file}-{i}.avi'
|
58 |
+
output = ffmpeg.concat(input_img, input_audio, v=1, a=1).output(final_filename).overwrite_output()
|
59 |
+
ffmpeg.run(output)
|
60 |
+
return final_filename
|
61 |
+
|
62 |
+
|
63 |
+
def make_short_films(image_file_list, audio_file_list, video_path, filename):
|
64 |
+
video_list = []
|
65 |
+
for i, img in enumerate(image_file_list):
|
66 |
+
final_filename = make_short_film(video_path, img, audio_file_list[i], filename, i)
|
67 |
+
video_list.append(final_filename)
|
68 |
+
return video_list
|
69 |
+
|
70 |
+
|
71 |
+
def summarize_film(file_list, path, filename):
|
72 |
+
output_file = f'{os.path.join(path, filename)}.mp4'
|
73 |
+
video_clips = []
|
74 |
+
intro = VideoFileClip(file_list[0])
|
75 |
+
intro_clip = make_title(intro)
|
76 |
+
video_clips.append(intro_clip)
|
77 |
+
for s in file_list[1::]:
|
78 |
+
video_clips.append(VideoFileClip(s).fx(vfx.fadein,1).fx(vfx.fadeout,1))
|
79 |
+
video_clips.append(make_credits(video_clips[0]))
|
80 |
+
final = concatenate_videoclips(video_clips)
|
81 |
+
|
82 |
+
# Write output to the file
|
83 |
+
final.write_videofile(output_file)
|
84 |
+
return output_file
|
85 |
+
# (
|
86 |
+
# ffmpeg
|
87 |
+
# .concat(t)
|
88 |
+
# .output(f'output-video.mp4')
|
89 |
+
# .overwrite_output()
|
90 |
+
# .run()
|
91 |
+
# )
|
filmr.py
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import os
|
3 |
+
from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath
|
4 |
+
import ffmpegmaker as film
|
5 |
+
import ffmpeg
|
6 |
+
import speech
|
7 |
+
import script
|
8 |
+
import image
|
9 |
+
from prompts import system_prompt, image_style_prompt
|
10 |
+
|
11 |
+
# #topic_array = ["utbildning", "transport", "ai", "energiförsörjning", "projekt Planet B", "bostäder", "mat"]
|
12 |
+
topic_array = ["utbildning"]
|
13 |
+
#topia_array = ["dystopic", "utopic"]
|
14 |
+
topia_array = ["dystopic"]
|
15 |
+
|
16 |
+
#File paths
|
17 |
+
parent_dir = os.getcwd()
|
18 |
+
base_audio_path = os.path.join(parent_dir, "audio")
|
19 |
+
base_image_path = os.path.join(parent_dir, "image")
|
20 |
+
base_script_path = os.path.join(parent_dir, "script")
|
21 |
+
base_video_path = os.path.join(parent_dir, "video")
|
22 |
+
base_demux_path = os.path.join(parent_dir, "demux")
|
23 |
+
|
24 |
+
|
25 |
+
def create_filelist(textfile_path, video_list):
|
26 |
+
with open(textfile_path, "w") as f:
|
27 |
+
for i, img in enumerate(video_list):
|
28 |
+
img_with_double = img.replace('\\', '\\\\')
|
29 |
+
f.write(f'file {img_with_double}\n')
|
30 |
+
|
31 |
+
|
32 |
+
def populate_filelists(audio_file_list, image_file_list, audio_file_length_list, topic, topia):
|
33 |
+
# Find all the newly created image files
|
34 |
+
p = Path(base_image_path)
|
35 |
+
for filepath in p.glob(f'{topic}-{topia}-*.png'):
|
36 |
+
image_file_list.append(filepath)
|
37 |
+
# Find all the newly created sound files
|
38 |
+
p = Path(base_audio_path)
|
39 |
+
for filepath in p.glob(f'{topic}-{topia}-*.mp3'):
|
40 |
+
audio_file_list.append(filepath)
|
41 |
+
# Store all the lengths of the newly created files
|
42 |
+
for file in audio_file_list:
|
43 |
+
duration = ffmpeg.probe(file)["format"]["duration"]
|
44 |
+
audio_file_length_list.append(duration)
|
45 |
+
|
46 |
+
|
47 |
+
def create_film(topic, topia):
|
48 |
+
audio_file_list = []
|
49 |
+
image_file_list = []
|
50 |
+
audio_file_length_list = []
|
51 |
+
# Create the script
|
52 |
+
# created_script = script.create_script(system_prompt, topic, topia)
|
53 |
+
# script.save_script(created_script, base_script_path, topic, topia)
|
54 |
+
# # Convert the script to JSON
|
55 |
+
# data = json.loads(created_script)
|
56 |
+
# sequences = data["sekvenser"]
|
57 |
+
# # Create all images and audio for each sequence.
|
58 |
+
# for i, sequence in enumerate(sequences):
|
59 |
+
# filename = f'{topic}-{topia}-{i}'
|
60 |
+
# speech.create_speech(sequence["beskrivning"], base_audio_path, filename)
|
61 |
+
# image.generate_DALLE_images(sequence["bild"], base_image_path, filename)
|
62 |
+
|
63 |
+
populate_filelists(audio_file_list, image_file_list, audio_file_length_list, topic, topia)
|
64 |
+
# Make short films
|
65 |
+
video_list = film.make_short_films(image_file_list, audio_file_list, base_video_path, f'{topic}-{topia}')
|
66 |
+
# Construct the demuxer file
|
67 |
+
#textfile_path = os.path.join(base_demux_path, f'{topic}-{topia}.txt')
|
68 |
+
#create_filelist(textfile_path, video_list)
|
69 |
+
# Make final film
|
70 |
+
output = film.summarize_film(video_list, base_video_path, f'{topic}-{topia}')
|
71 |
+
return output
|
72 |
+
|
73 |
+
|
74 |
+
def create_multiple_films(topic_list, topia_list):
|
75 |
+
for topic in topic_list:
|
76 |
+
for topia in topia_list:
|
77 |
+
create_film(topic, topia)
|
78 |
+
|
79 |
+
|
80 |
+
if __name__ == '__main__':
|
81 |
+
create_multiple_films(topic_array, topia_array)
|
82 |
+
|
83 |
+
# with open(textfile_path, "w") as f:
|
84 |
+
# for i, img in enumerate(image_file_list):
|
85 |
+
# f.write(f'file {img}\n')
|
86 |
+
# duration = math.ceil(float(audio_file_length_list[i]))
|
87 |
+
# f.write(f'duration {duration}\n')
|
gradio_run.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import filmr
|
3 |
+
|
4 |
+
|
5 |
+
|
6 |
+
|
7 |
+
def greet(topic, topia):
|
8 |
+
output = filmr.create_film(topic, topia)
|
9 |
+
|
10 |
+
return output
|
11 |
+
|
12 |
+
|
13 |
+
filmr_gui = gr.Interface(
|
14 |
+
fn=greet,
|
15 |
+
inputs=["text", "text"],
|
16 |
+
outputs=["download_button"],
|
17 |
+
title="Filmr",
|
18 |
+
description="Create a film with the help of GPT4",
|
19 |
+
)
|
20 |
+
|
21 |
+
filmr_gui.launch(allowed_paths=["D:\repos\filmr"])
|
image.py
ADDED
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'''
|
2 |
+
DALL-E image generation example for openai>1.2.3, saves requested images as files
|
3 |
+
-- not a code utility, has no input or return
|
4 |
+
|
5 |
+
# example pydantic models returned by client.images.generate(**img_params):
|
6 |
+
## - when called with "response_format": "url":
|
7 |
+
images_response = ImagesResponse(created=1699713836, data=[Image(b64_json=None, revised_prompt=None, url='https://oaidalleapiprodscus.blob.core.windows.net/private/org-abcd/user-abcd/img-12345.png?st=2023-11-11T13%3A43%3A56Z&se=2023-11-11T15%3A43%3A56Z&sp=r&sv=2021-08-06&sr=b&rscd=inline&rsct=image/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2023-11-10T21%3A41%3A11Z&ske=2023-11-11T21%3A41%3A11Z&sks=b&skv=2021-08-06&sig=%2BUjl3f6Vdz3u0oRSuERKPzPhFRf7qO8RjwSPGsrQ/d8%3D')])
|
8 |
+
|
9 |
+
requires:
|
10 |
+
pip install --upgrade openai
|
11 |
+
pip install pillow
|
12 |
+
'''
|
13 |
+
import os
|
14 |
+
from io import BytesIO
|
15 |
+
import openai # for handling error types
|
16 |
+
from datetime import datetime # for formatting date returned with images
|
17 |
+
import base64 # for decoding images if recieved in the reply
|
18 |
+
import requests # for downloading images from URLs
|
19 |
+
from PIL import Image # pillow, for processing image types
|
20 |
+
from prompts import image_style_prompt
|
21 |
+
from dotenv import load_dotenv
|
22 |
+
load_dotenv()
|
23 |
+
|
24 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
25 |
+
|
26 |
+
os.environ["OPENAI_API_KEY"] = api_key
|
27 |
+
|
28 |
+
def old_package(version, minimum): # Block old openai python libraries before today's
|
29 |
+
version_parts = list(map(int, version.split(".")))
|
30 |
+
minimum_parts = list(map(int, minimum.split(".")))
|
31 |
+
return version_parts < minimum_parts
|
32 |
+
|
33 |
+
if old_package(openai.__version__, "1.2.3"):
|
34 |
+
raise ValueError(f"Error: OpenAI version {openai.__version__}"
|
35 |
+
" is less than the minimum version 1.2.3\n\n"
|
36 |
+
">>You should run 'pip install --upgrade openai')")
|
37 |
+
|
38 |
+
from openai import OpenAI
|
39 |
+
# client = OpenAI(api_key="sk-xxxxx") # don't do this, OK?
|
40 |
+
client = OpenAI() # will use environment variable "OPENAI_API_KEY"
|
41 |
+
|
42 |
+
|
43 |
+
|
44 |
+
def generate_DALLE_images(user_prompt, path, filename):
|
45 |
+
|
46 |
+
prompt = (
|
47 |
+
f"Subject: {user_prompt} " # use the space at end
|
48 |
+
f"Style: {image_style_prompt}" # this is implicit line continuation
|
49 |
+
)
|
50 |
+
|
51 |
+
image_params = {
|
52 |
+
"model": "dall-e-3", # Defaults to dall-e-2
|
53 |
+
"n": 1, # Between 2 and 10 is only for DALL-E 2
|
54 |
+
"size": "1024x1024", # 256x256, 512x512 only for DALL-E 2 - not much cheaper
|
55 |
+
"prompt": prompt, # DALL-E 3: max 4000 characters, DALL-E 2: max 1000
|
56 |
+
"user": "myName", # pass a customer ID to OpenAI for abuse monitoring
|
57 |
+
}
|
58 |
+
|
59 |
+
## -- You can uncomment the lines below to include these non-default parameters --
|
60 |
+
|
61 |
+
image_params.update({"response_format": "b64_json"}) # defaults to "url" for separate download
|
62 |
+
|
63 |
+
## -- DALL-E 3 exclusive parameters --
|
64 |
+
image_params.update({"model": "dall-e-3"}) # Upgrade the model name to dall-e-3
|
65 |
+
image_params.update({"size": "1792x1024"}) # 1792x1024 or 1024x1792 available for DALL-E 3
|
66 |
+
# image_params.update({"quality": "hd"}) # quality at 2x the price, defaults to "standard"
|
67 |
+
# image_params.update({"style": "natural"}) # defaults to "vivid"
|
68 |
+
|
69 |
+
print(f'Generating image {filename}')
|
70 |
+
try:
|
71 |
+
images_response = client.images.generate(**image_params)
|
72 |
+
except openai.APIConnectionError as e:
|
73 |
+
print("Server connection error: {e.__cause__}") # from httpx.
|
74 |
+
raise
|
75 |
+
except openai.RateLimitError as e:
|
76 |
+
print(f"OpenAI RATE LIMIT error {e.status_code}: (e.response)")
|
77 |
+
raise
|
78 |
+
except openai.APIStatusError as e:
|
79 |
+
print(f"OpenAI STATUS error {e.status_code}: (e.response)")
|
80 |
+
raise
|
81 |
+
except openai.BadRequestError as e:
|
82 |
+
print(f"OpenAI BAD REQUEST error {e.status_code}: (e.response)")
|
83 |
+
raise
|
84 |
+
except Exception as e:
|
85 |
+
print(f"An unexpected error occurred: {e}")
|
86 |
+
raise
|
87 |
+
|
88 |
+
# make a file name prefix from date-time of response
|
89 |
+
#images_dt = datetime.utcfromtimestamp(images_response.created)
|
90 |
+
#img_filename = images_dt.strftime('DALLE-%Y%m%d_%H%M%S') # like 'DALLE-20231111_144356'
|
91 |
+
img_filename = file_path = os.path.join(path, f'{filename}.png')
|
92 |
+
|
93 |
+
# get the prompt used if rewritten by dall-e-3, null if unchanged by AI
|
94 |
+
revised_prompt = images_response.data[0].revised_prompt
|
95 |
+
|
96 |
+
# get out all the images in API return, whether url or base64
|
97 |
+
# note the use of pydantic "model.data" style reference and its model_dump() method
|
98 |
+
image_url_list = []
|
99 |
+
image_data_list = []
|
100 |
+
for image in images_response.data:
|
101 |
+
image_url_list.append(image.model_dump()["url"])
|
102 |
+
image_data_list.append(image.model_dump()["b64_json"])
|
103 |
+
|
104 |
+
# Initialize an empty list to store the Image objects
|
105 |
+
image_objects = []
|
106 |
+
|
107 |
+
# Check whether lists contain urls that must be downloaded or b64_json images
|
108 |
+
if image_url_list and all(image_url_list):
|
109 |
+
# Download images from the urls
|
110 |
+
for i, url in enumerate(image_url_list):
|
111 |
+
while True:
|
112 |
+
try:
|
113 |
+
print(f"getting URL: {url}")
|
114 |
+
response = requests.get(url)
|
115 |
+
response.raise_for_status() # Raises stored HTTPError, if one occurred.
|
116 |
+
except requests.HTTPError as e:
|
117 |
+
print(f"Failed to download image from {url}. Error: {e.response.status_code}")
|
118 |
+
retry = input("Retry? (y/n): ") # ask script user if image url is bad
|
119 |
+
if retry.lower() in ["n", "no"]: # could wait a bit if not ready
|
120 |
+
raise
|
121 |
+
else:
|
122 |
+
continue
|
123 |
+
break
|
124 |
+
image_objects.append(Image.open(BytesIO(response.content))) # Append the Image object to the list
|
125 |
+
image_objects[i].save(f"{img_filename}")
|
126 |
+
print(f"Saving image to {img_filename}")
|
127 |
+
elif image_data_list and all(image_data_list): # if there is b64 data
|
128 |
+
# Convert "b64_json" data to png file
|
129 |
+
for i, data in enumerate(image_data_list):
|
130 |
+
image_objects.append(Image.open(BytesIO(base64.b64decode(data)))) # Append the Image object to the list
|
131 |
+
image_objects[i].save(f"{img_filename}")
|
132 |
+
print(f"Saving image to {img_filename}")
|
133 |
+
else:
|
134 |
+
print("No image data was obtained. Maybe bad code?")
|
135 |
+
|
136 |
+
|
137 |
+
|
prompts.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
system_prompt = '''
|
2 |
+
Du är en futurist och en dokumentärskapare. Ditt jobb är att skriva ett manus till en dokumentär om hur världen kommer ser ut om 50 år.
|
3 |
+
Manuset ska vara begränsad till ett visst område.
|
4 |
+
Beroende på propmpten är du antingen utopikst eller dystopiskt.
|
5 |
+
Ditt manus formatteras som en JSON fil.
|
6 |
+
Du ger förslag till text men även till bilderna som ska vara med i doumentären.
|
7 |
+
När du beskriver bilderna gör du det genom att skriver ett DALLE-E prompt som skulle genera den bilden.
|
8 |
+
Bilden ska se verkligt ut.
|
9 |
+
Manuset ska består ett antal sekvensker coh varje sekvens ska ha JSON keys "titel", "beskrivning" och "bild".
|
10 |
+
Beskrivningen ska vara utförlig.
|
11 |
+
Det ska även finnas en sekvens för introduktion och avslutning.
|
12 |
+
ALla sekvenser placeras i en array som heter sekvenser.
|
13 |
+
'''
|
14 |
+
|
15 |
+
image_style_prompt = '''
|
16 |
+
90s flash photo 35mm iso 200 scratches and dust chromatic aberration slow shutter speed drippy candid
|
17 |
+
'''
|
requirements.txt
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
openai~=1.25.0
|
2 |
+
langchain~=0.1.17
|
3 |
+
ffmpeg-python
|
4 |
+
future~=1.0.0
|
5 |
+
requests~=2.31.0
|
6 |
+
elevenlabs~=1.2.1
|
7 |
+
ffmpeg~=1.4
|
8 |
+
pillow~=10.3.0
|
9 |
+
python-dotenv~=1.0.1
|
10 |
+
gradio~=4.31.0
|
11 |
+
moviepy~=1.0.3
|
script.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from langchain_openai import ChatOpenAI
|
3 |
+
from langchain_core.prompts import ChatPromptTemplate
|
4 |
+
from langchain_core.output_parsers import StrOutputParser
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
10 |
+
|
11 |
+
llm = ChatOpenAI(api_key=api_key)
|
12 |
+
|
13 |
+
|
14 |
+
def create_script(prompt, topic, topia):
|
15 |
+
print(f'Writing script about {topic} {topia}')
|
16 |
+
final_prompt = ChatPromptTemplate.from_messages([
|
17 |
+
("system", '{prompt}'),
|
18 |
+
("user", '{topic}'),
|
19 |
+
("user", '{topia}')
|
20 |
+
])
|
21 |
+
output_parser = StrOutputParser()
|
22 |
+
chain = final_prompt | llm | output_parser
|
23 |
+
response = chain.invoke({"prompt": prompt, "topic": topic, "topia": topia})
|
24 |
+
return response
|
25 |
+
|
26 |
+
|
27 |
+
def save_script(script, path, topic, topia):
|
28 |
+
filename = f'{topic}-{topia}.txt'
|
29 |
+
file_path = os.path.join(path, filename)
|
30 |
+
print(f'Saving script to {file_path}')
|
31 |
+
with open(file_path, "w") as f:
|
32 |
+
f.write(script)
|
script/utbildning-dystopi.txt
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"sekvenser": [
|
3 |
+
{
|
4 |
+
"titel": "Introduktion",
|
5 |
+
"beskrivning": "V�lkommen till v�r dokument�r om utbildningens dystopiska framtid! I denna v�rld har teknologin tagit �ver alltmer av v�ra liv, och utbildningssystemet �r ingen undantag. Vi ska utforska hur utbildningen har f�r�ndrats och vilka konsekvenser det har haft f�r samh�llet.",
|
6 |
+
"bild": "En futuristisk skolbyggnad omgiven av h�ga st�ngsel och �vervakningskameror, d�r elever i enhetliga dr�kter g�r i rader genom en s�kerhetskontroll. En stor holografisk sk�rm visar en str�ng l�rare som �vervakar dem."
|
7 |
+
},
|
8 |
+
{
|
9 |
+
"titel": "�vervakning och kontroll",
|
10 |
+
"beskrivning": "I framtida utbildningssystemet har �vervakningen och kontrollen �kat drastiskt. Eleverna �r st�ndigt under uppsikt, b�de fysiskt och digitalt. Deras beteende och prestationer �vervakas noggrant f�r att s�kerst�lla konformitet och lydnad.",
|
11 |
+
"bild": "En ung elev sitter ensam vid ett skrivbord med en VR-hj�lm p� huvudet, medan en robotl�rare med kamera�gon bevakar varje r�relse. En sk�rm visar elevens biometriska data och prestationer i realtid."
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"titel": "Indoktrinering och anpassning",
|
15 |
+
"beskrivning": "I utbildningssystemet har indoktrinering och anpassning blivit centrala. Eleverna matas med en standardiserad l�roplan som fr�mjar lydnad och anpassning till det r�dande samh�llssystemet. Kreativitet och individualitet �r inte v�lkommet.",
|
16 |
+
"bild": "En grupp elever sitter i en klassrumspod, d�r en stor sk�rm projicerar riktlinjer f�r beteende och tankes�tt. Elevernas ansikten �r uttrycksl�sa och likformiga, visar ingen tecken p� individualitet."
|
17 |
+
},
|
18 |
+
{
|
19 |
+
"titel": "Teknologins dominans",
|
20 |
+
"beskrivning": "Teknologin har blivit den prim�ra undervisningsmetoden i utbildningssystemet. Eleverna spenderar st�rre delen av sin tid framf�r sk�rmar och interagerar med AI-drivna program ist�llet f�r med verkliga l�rare. M�nsklig interaktion �r minimal.",
|
21 |
+
"bild": "En stor sal fylld med elever som sitter vid skrivbord med sk�rmar framf�r sig. En gigantisk holografisk l�rare sv�var �ver dem och ger instruktioner. Eleverna �r f�rdjupade i sina sk�rmar och verkar omedvetna om sin omgivning."
|
22 |
+
},
|
23 |
+
{
|
24 |
+
"titel": "Avslutning",
|
25 |
+
"beskrivning": "Vi har sett hur utbildningssystemet har f�rvandlats till en dystopisk milj� d�r kontroll, anpassning och teknologisk dominans styr. Fr�gan �r om det fortfarande finns hopp om en mer humanistisk och inkluderande framtid f�r utbildningen.",
|
26 |
+
"bild": "En ensam elev st�r framf�r en gigantisk mur av digitala sk�rmar som visar algoritmer och data. Trots omgivningen av teknologi ser eleven best�md ut och blickar mot horisonten, kanske med en antydan till hopp i �gonen."
|
27 |
+
}
|
28 |
+
]
|
29 |
+
}
|
script/utbildning-dystopic.txt
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"sekvenser": [
|
3 |
+
{
|
4 |
+
"titel": "Introduktion",
|
5 |
+
"beskrivning": "I denna dokument�r ska vi utforska hur utbildningssystemet har f�r�ndrats och utvecklats under de senaste 50 �ren. Vi kommer att se p� hur teknologin har p�verkat l�randet, tillg�ngen till utbildning och vilka konsekvenser detta har haft f�r samh�llet.",
|
6 |
+
"bild": "En skolbyggnad �vergiven och t�ckt av mossa, med trasiga f�nster och v�xtlighet som tar �ver. En symbol f�r det gamla utbildningssystemet som har l�mnats bakom."
|
7 |
+
},
|
8 |
+
{
|
9 |
+
"titel": "Digitalisering av utbildning",
|
10 |
+
"beskrivning": "I framtiden har utbildning blivit helt digitaliserad. Elever och studenter deltar i virtuella klassrum fr�n sina hem eller andra platser �ver hela v�rlden. Genom anv�ndning av avancerade teknologier som virtuell verklighet och artificiell intelligens har undervisningen blivit mer interaktiv och anpassningsbar.",
|
11 |
+
"bild": "En ung student med en VR-headset sitter i sitt rum och deltar i en virtuell biologi lektion. Genom headsetet ser vi en realistisk modell av en cell som denne unders�ker."
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"titel": "Sociala konsekvenser",
|
15 |
+
"beskrivning": "Den digitala utbildningen har lett till en �kad isolering bland unga m�nniskor. Fysiska skolor har blivit ovanliga och interaktionen mellan elever och l�rare har minskat drastiskt. M�nga unga saknar nu f�rdigheter som kr�vs f�r att samarbeta och kommunicera ansikte mot ansikte.",
|
16 |
+
"bild": "En grupp ton�ringar sitter ensamma med sina datorer i en tom skolmatsal. Ingen pratar med varandra och alla �r f�rsjunkna i sina egna virtuella v�rldar."
|
17 |
+
},
|
18 |
+
{
|
19 |
+
"titel": "Ekonomiska klyftor",
|
20 |
+
"beskrivning": "Trots den �kade tillg�ngligheten till digital utbildning har ekonomiska klyftor blivit tydligare. Endast de som har r�d med den senaste teknologin och snabb internetanslutning har m�jlighet att ta del av h�gkvalitativ utbildning. De som kommer fr�n mindre bemedlade familjer har sv�rt att konkurrera p� den digitala utbildningsmarknaden.",
|
21 |
+
"bild": "En ung student tittar avundsjukt p� en annan student som har den senaste modellen av en utbildningsrobot. Den f�rsta studenten anv�nder fortfarande en gammal och l�ngsam dator."
|
22 |
+
},
|
23 |
+
{
|
24 |
+
"titel": "Avslutning",
|
25 |
+
"beskrivning": "I denna dokument�r har vi sett hur utbildning har f�r�ndrats dramatiskt under de senaste 50 �ren. Trots de teknologiska framstegen har det uppst�tt nya problem och utmaningar i utbildningssystemet. Det �r viktigt att reflektera �ver konsekvenserna av digitaliseringen och arbeta f�r att skapa ett mer j�mlikt och inkluderande utbildningssystem f�r framtiden.",
|
26 |
+
"bild": "En gammal skolbok ligger ensam p� en b�nk i en �vergiven skolbyggnad. Texten p� boken �r suddig och otydlig, en symbol f�r det f�rflutna som h�ller p� att blekna bort."
|
27 |
+
}
|
28 |
+
]
|
29 |
+
}
|
speech.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from elevenlabs import play, save
|
2 |
+
from elevenlabs.client import ElevenLabs
|
3 |
+
import os
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
|
6 |
+
load_dotenv()
|
7 |
+
|
8 |
+
api_key = os.getenv("ELEVEN_API_KEY")
|
9 |
+
|
10 |
+
|
11 |
+
def create_speech(text, path, filename):
|
12 |
+
client = ElevenLabs(
|
13 |
+
api_key=api_key,
|
14 |
+
)
|
15 |
+
print(f'Generating audio {filename}')
|
16 |
+
audio = client.generate(
|
17 |
+
text=text,
|
18 |
+
voice="Rachel",
|
19 |
+
model="eleven_multilingual_v2"
|
20 |
+
)
|
21 |
+
|
22 |
+
file_path = os.path.join(path, f'{filename}.mp3')
|
23 |
+
print(f'Saving audio to {file_path}')
|
24 |
+
save(audio, file_path)
|
youtube.py
ADDED
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/python
|
2 |
+
|
3 |
+
import httplib
|
4 |
+
import httplib2
|
5 |
+
import os
|
6 |
+
import random
|
7 |
+
import sys
|
8 |
+
import time
|
9 |
+
|
10 |
+
from apiclient.discovery import build
|
11 |
+
from apiclient.errors import HttpError
|
12 |
+
from apiclient.http import MediaFileUpload
|
13 |
+
from oauth2client.client import flow_from_clientsecrets
|
14 |
+
from oauth2client.file import Storage
|
15 |
+
from oauth2client.tools import argparser, run_flow
|
16 |
+
|
17 |
+
|
18 |
+
# Explicitly tell the underlying HTTP transport library not to retry, since
|
19 |
+
# we are handling retry logic ourselves.
|
20 |
+
httplib2.RETRIES = 1
|
21 |
+
|
22 |
+
# Maximum number of times to retry before giving up.
|
23 |
+
MAX_RETRIES = 10
|
24 |
+
|
25 |
+
# Always retry when these exceptions are raised.
|
26 |
+
RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError, httplib.NotConnected,
|
27 |
+
httplib.IncompleteRead, httplib.ImproperConnectionState,
|
28 |
+
httplib.CannotSendRequest, httplib.CannotSendHeader,
|
29 |
+
httplib.ResponseNotReady, httplib.BadStatusLine)
|
30 |
+
|
31 |
+
# Always retry when an apiclient.errors.HttpError with one of these status
|
32 |
+
# codes is raised.
|
33 |
+
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
|
34 |
+
|
35 |
+
# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
|
36 |
+
# the OAuth 2.0 information for this application, including its client_id and
|
37 |
+
# client_secret. You can acquire an OAuth 2.0 client ID and client secret from
|
38 |
+
# the Google API Console at
|
39 |
+
# https://console.cloud.google.com/.
|
40 |
+
# Please ensure that you have enabled the YouTube Data API for your project.
|
41 |
+
# For more information about using OAuth2 to access the YouTube Data API, see:
|
42 |
+
# https://developers.google.com/youtube/v3/guides/authentication
|
43 |
+
# For more information about the client_secrets.json file format, see:
|
44 |
+
# https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
|
45 |
+
CLIENT_SECRETS_FILE = "client_secrets.json"
|
46 |
+
|
47 |
+
# This OAuth 2.0 access scope allows an application to upload files to the
|
48 |
+
# authenticated user's YouTube channel, but doesn't allow other types of access.
|
49 |
+
YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload"
|
50 |
+
YOUTUBE_API_SERVICE_NAME = "youtube"
|
51 |
+
YOUTUBE_API_VERSION = "v3"
|
52 |
+
|
53 |
+
# This variable defines a message to display if the CLIENT_SECRETS_FILE is
|
54 |
+
# missing.
|
55 |
+
MISSING_CLIENT_SECRETS_MESSAGE = """
|
56 |
+
WARNING: Please configure OAuth 2.0
|
57 |
+
|
58 |
+
To make this sample run you will need to populate the client_secrets.json file
|
59 |
+
found at:
|
60 |
+
|
61 |
+
%s
|
62 |
+
|
63 |
+
with information from the API Console
|
64 |
+
https://console.cloud.google.com/
|
65 |
+
|
66 |
+
For more information about the client_secrets.json file format, please visit:
|
67 |
+
https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
|
68 |
+
""" % os.path.abspath(os.path.join(os.path.dirname(__file__),
|
69 |
+
CLIENT_SECRETS_FILE))
|
70 |
+
|
71 |
+
VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")
|
72 |
+
|
73 |
+
|
74 |
+
def get_authenticated_service(args):
|
75 |
+
flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE,
|
76 |
+
scope=YOUTUBE_UPLOAD_SCOPE,
|
77 |
+
message=MISSING_CLIENT_SECRETS_MESSAGE)
|
78 |
+
|
79 |
+
storage = Storage("%s-oauth2.json" % sys.argv[0])
|
80 |
+
credentials = storage.get()
|
81 |
+
|
82 |
+
if credentials is None or credentials.invalid:
|
83 |
+
credentials = run_flow(flow, storage, args)
|
84 |
+
|
85 |
+
return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
|
86 |
+
http=credentials.authorize(httplib2.Http()))
|
87 |
+
|
88 |
+
def initialize_upload(youtube, options):
|
89 |
+
tags = None
|
90 |
+
if options.keywords:
|
91 |
+
tags = options.keywords.split(",")
|
92 |
+
|
93 |
+
body=dict(
|
94 |
+
snippet=dict(
|
95 |
+
title=options.title,
|
96 |
+
description=options.description,
|
97 |
+
tags=tags,
|
98 |
+
categoryId=options.category
|
99 |
+
),
|
100 |
+
status=dict(
|
101 |
+
privacyStatus=options.privacyStatus
|
102 |
+
)
|
103 |
+
)
|
104 |
+
|
105 |
+
# Call the API's videos.insert method to create and upload the video.
|
106 |
+
insert_request = youtube.videos().insert(
|
107 |
+
part=",".join(body.keys()),
|
108 |
+
body=body,
|
109 |
+
# The chunksize parameter specifies the size of each chunk of data, in
|
110 |
+
# bytes, that will be uploaded at a time. Set a higher value for
|
111 |
+
# reliable connections as fewer chunks lead to faster uploads. Set a lower
|
112 |
+
# value for better recovery on less reliable connections.
|
113 |
+
#
|
114 |
+
# Setting "chunksize" equal to -1 in the code below means that the entire
|
115 |
+
# file will be uploaded in a single HTTP request. (If the upload fails,
|
116 |
+
# it will still be retried where it left off.) This is usually a best
|
117 |
+
# practice, but if you're using Python older than 2.6 or if you're
|
118 |
+
# running on App Engine, you should set the chunksize to something like
|
119 |
+
# 1024 * 1024 (1 megabyte).
|
120 |
+
media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
|
121 |
+
)
|
122 |
+
|
123 |
+
resumable_upload(insert_request)
|
124 |
+
|
125 |
+
# This method implements an exponential backoff strategy to resume a
|
126 |
+
# failed upload.
|
127 |
+
def resumable_upload(insert_request):
|
128 |
+
response = None
|
129 |
+
error = None
|
130 |
+
retry = 0
|
131 |
+
while response is None:
|
132 |
+
try:
|
133 |
+
print "Uploading file..."
|
134 |
+
status, response = insert_request.next_chunk()
|
135 |
+
if response is not None:
|
136 |
+
if 'id' in response:
|
137 |
+
print "Video id '%s' was successfully uploaded." % response['id']
|
138 |
+
else:
|
139 |
+
exit("The upload failed with an unexpected response: %s" % response)
|
140 |
+
except HttpError, e:
|
141 |
+
if e.resp.status in RETRIABLE_STATUS_CODES:
|
142 |
+
error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status,
|
143 |
+
e.content)
|
144 |
+
else:
|
145 |
+
raise
|
146 |
+
except RETRIABLE_EXCEPTIONS, e:
|
147 |
+
error = "A retriable error occurred: %s" % e
|
148 |
+
|
149 |
+
if error is not None:
|
150 |
+
print error
|
151 |
+
retry += 1
|
152 |
+
if retry > MAX_RETRIES:
|
153 |
+
exit("No longer attempting to retry.")
|
154 |
+
|
155 |
+
max_sleep = 2 ** retry
|
156 |
+
sleep_seconds = random.random() * max_sleep
|
157 |
+
print "Sleeping %f seconds and then retrying..." % sleep_seconds
|
158 |
+
time.sleep(sleep_seconds)
|
159 |
+
|
160 |
+
if __name__ == '__main__':
|
161 |
+
argparser.add_argument("--file", required=True, help="Video file to upload")
|
162 |
+
argparser.add_argument("--title", help="Video title", default="Test Title")
|
163 |
+
argparser.add_argument("--description", help="Video description",
|
164 |
+
default="Test Description")
|
165 |
+
argparser.add_argument("--category", default="22",
|
166 |
+
help="Numeric video category. " +
|
167 |
+
"See https://developers.google.com/youtube/v3/docs/videoCategories/list")
|
168 |
+
argparser.add_argument("--keywords", help="Video keywords, comma separated",
|
169 |
+
default="")
|
170 |
+
argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES,
|
171 |
+
default=VALID_PRIVACY_STATUSES[0], help="Video privacy status.")
|
172 |
+
args = argparser.parse_args()
|
173 |
+
|
174 |
+
if not os.path.exists(args.file):
|
175 |
+
exit("Please specify a valid file using the --file= parameter.")
|
176 |
+
|
177 |
+
youtube = get_authenticated_service(args)
|
178 |
+
try:
|
179 |
+
initialize_upload(youtube, args)
|
180 |
+
except HttpError, e:
|
181 |
+
print "An HTTP error %d occurred:\n%s" % (e.resp.status, e.content)
|