Spaces:
Sleeping
Sleeping
Topallaj Denis
commited on
Commit
·
aff0a09
1
Parent(s):
2009d4b
init
Browse files- .gitignore +163 -0
- Dockerfile +27 -0
- README.md +36 -3
- data/basic/audio.mp3 +0 -0
- data/basic/subtitles.srt +180 -0
- data/minecraft/audio.mp3 +0 -0
- data/minecraft/subtitles.srt +204 -0
- data/subway/audio.mp3 +0 -0
- data/subway/subtitles.srt +80 -0
- endpoints/__init__.py +7 -0
- endpoints/basic.py +44 -0
- endpoints/minecraft.py +45 -0
- endpoints/subway.py +48 -0
- endpoints/utils/remove_content_from_dir.py +13 -0
- endpoints/video_editor_cut.py +70 -0
- generate_basic_subtitles.py +54 -0
- generate_minecraft_subtitles.py +113 -0
- generate_subway_subtitles.py +140 -0
- requirements.txt +14 -0
- server.py +23 -0
- utils/create_subtitles.py +25 -0
- utils/text_to_speech.py +6 -0
- utils/transcribe_audio.py +7 -0
- utils/video_to_audio.py +14 -0
.gitignore
ADDED
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
# pdm
|
105 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
106 |
+
#pdm.lock
|
107 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
108 |
+
# in version control.
|
109 |
+
# https://pdm.fming.dev/#use-with-ide
|
110 |
+
.pdm.toml
|
111 |
+
|
112 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
113 |
+
__pypackages__/
|
114 |
+
|
115 |
+
# Celery stuff
|
116 |
+
celerybeat-schedule
|
117 |
+
celerybeat.pid
|
118 |
+
|
119 |
+
# SageMath parsed files
|
120 |
+
*.sage.py
|
121 |
+
|
122 |
+
# Environments
|
123 |
+
.env
|
124 |
+
.venv
|
125 |
+
env/
|
126 |
+
venv/
|
127 |
+
ENV/
|
128 |
+
env.bak/
|
129 |
+
venv.bak/
|
130 |
+
|
131 |
+
# Spyder project settings
|
132 |
+
.spyderproject
|
133 |
+
.spyproject
|
134 |
+
|
135 |
+
# Rope project settings
|
136 |
+
.ropeproject
|
137 |
+
|
138 |
+
# mkdocs documentation
|
139 |
+
/site
|
140 |
+
|
141 |
+
# mypy
|
142 |
+
.mypy_cache/
|
143 |
+
.dmypy.json
|
144 |
+
dmypy.json
|
145 |
+
|
146 |
+
# Pyre type checker
|
147 |
+
.pyre/
|
148 |
+
|
149 |
+
# pytype static type analyzer
|
150 |
+
.pytype/
|
151 |
+
|
152 |
+
# Cython debug symbols
|
153 |
+
cython_debug/
|
154 |
+
|
155 |
+
# PyCharm
|
156 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
157 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
158 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
159 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
160 |
+
#.idea/
|
161 |
+
|
162 |
+
Pipfile
|
163 |
+
Pipfile.lock
|
Dockerfile
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
|
2 |
+
# you will also find guides on how best to write your Dockerfile
|
3 |
+
|
4 |
+
FROM python:3.11
|
5 |
+
|
6 |
+
WORKDIR /code
|
7 |
+
|
8 |
+
COPY ./requirements.txt /code/requirements.txt
|
9 |
+
|
10 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
11 |
+
|
12 |
+
RUN docker pull jrottenberg/ffmpeg
|
13 |
+
|
14 |
+
COPY . .
|
15 |
+
|
16 |
+
RUN useradd -m -u 1000 user
|
17 |
+
|
18 |
+
USER user
|
19 |
+
|
20 |
+
ENV HOME=/home/user \
|
21 |
+
PATH=/home/user/.local/bin:$PATH
|
22 |
+
|
23 |
+
WORKDIR $HOME/app
|
24 |
+
|
25 |
+
COPY --chown=user . $HOME/app
|
26 |
+
|
27 |
+
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
@@ -1,10 +1,43 @@
|
|
1 |
---
|
2 |
title: BrainRotTok
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
---
|
9 |
|
10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
title: BrainRotTok
|
3 |
+
emoji: 🧟
|
4 |
+
colorFrom: green
|
5 |
+
colorTo: pink
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
---
|
9 |
|
10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
11 |
+
|
12 |
+
# BrainRotTok
|
13 |
+
|
14 |
+
## What is BrainRotTok?
|
15 |
+
|
16 |
+
BrainRotTok allows you to create those iconic TikTok videos with subtitles. There are different types of videos you can create, you can find the list of them below in the "What can you do with this?" section.
|
17 |
+
|
18 |
+
The same project is also available on GitHub: [Detopall/BrainRotTok](https://github.com/Detopall/BrainRotTok)
|
19 |
+
|
20 |
+
## What is brain rot?
|
21 |
+
|
22 |
+
Brain rot is a term used to describe the feeling of your brain rotting away after watching those highly addictive, short videos on TikTok. It is also used to describe the content that causes this feeling. Brain rot content is usually low effort, low quality, and highly addictive.
|
23 |
+
|
24 |
+
[Urban Dictionary](https://www.urbandictionary.com/define.php?term=Brainrot%20Content)
|
25 |
+
|
26 |
+
## Why did I make this?
|
27 |
+
|
28 |
+
I kept seeing these videos on TikTok and I thought it would be fun to make a website that allows you to create these videos. I also wanted to use FFmpeg and OpenAI's Whisper API, so I thought this would be a good project to do that.
|
29 |
+
|
30 |
+
## What can you do with this?
|
31 |
+
|
32 |
+
- **Basic**: a video with subtitles underneath
|
33 |
+
- **Subway Surfers**: a top and bottom video with subtitles for the bottom video and the top video is muted
|
34 |
+
- **Minecraft Reddit**: a background video with subtitles (provided by you) in the middle and the subtitles are spoken by a text-to-speech voice
|
35 |
+
|
36 |
+
## What technologies are used?
|
37 |
+
|
38 |
+
- React (with TypeScript)
|
39 |
+
- Python
|
40 |
+
- FastAPI
|
41 |
+
- Whisper (OpenAI)
|
42 |
+
- FFmpeg
|
43 |
+
- ImageMagick
|
data/basic/audio.mp3
ADDED
Binary file (319 kB). View file
|
|
data/basic/subtitles.srt
ADDED
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
1.1
|
2 |
+
00:00:00,000 --> 00:00:00,020
|
3 |
+
I
|
4 |
+
|
5 |
+
1.2
|
6 |
+
00:00:00,020 --> 00:00:00,100
|
7 |
+
know
|
8 |
+
|
9 |
+
1.3
|
10 |
+
00:00:00,100 --> 00:00:00,280
|
11 |
+
what
|
12 |
+
|
13 |
+
1.4
|
14 |
+
00:00:00,280 --> 00:00:00,460
|
15 |
+
you
|
16 |
+
|
17 |
+
1.5
|
18 |
+
00:00:00,460 --> 00:00:00,800
|
19 |
+
want,
|
20 |
+
|
21 |
+
1.6
|
22 |
+
00:00:01,179 --> 00:00:01,480
|
23 |
+
at
|
24 |
+
|
25 |
+
1.7
|
26 |
+
00:00:01,480 --> 00:00:01,620
|
27 |
+
the
|
28 |
+
|
29 |
+
1.8
|
30 |
+
00:00:01,620 --> 00:00:01,840
|
31 |
+
meeting,
|
32 |
+
|
33 |
+
1.9
|
34 |
+
00:00:02,240 --> 00:00:02,439
|
35 |
+
if
|
36 |
+
|
37 |
+
1.10
|
38 |
+
00:00:02,439 --> 00:00:02,580
|
39 |
+
you
|
40 |
+
|
41 |
+
1.11
|
42 |
+
00:00:02,580 --> 00:00:02,819
|
43 |
+
don't
|
44 |
+
|
45 |
+
1.12
|
46 |
+
00:00:02,819 --> 00:00:02,980
|
47 |
+
have
|
48 |
+
|
49 |
+
1.13
|
50 |
+
00:00:02,980 --> 00:00:03,100
|
51 |
+
a
|
52 |
+
|
53 |
+
1.14
|
54 |
+
00:00:03,100 --> 00:00:03,299
|
55 |
+
game
|
56 |
+
|
57 |
+
1.15
|
58 |
+
00:00:03,299 --> 00:00:03,620
|
59 |
+
plan.
|
60 |
+
|
61 |
+
2.1
|
62 |
+
00:00:03,859 --> 00:00:03,980
|
63 |
+
He
|
64 |
+
|
65 |
+
2.2
|
66 |
+
00:00:03,980 --> 00:00:04,139
|
67 |
+
may
|
68 |
+
|
69 |
+
2.3
|
70 |
+
00:00:04,139 --> 00:00:04,280
|
71 |
+
have
|
72 |
+
|
73 |
+
2.4
|
74 |
+
00:00:04,280 --> 00:00:04,419
|
75 |
+
a
|
76 |
+
|
77 |
+
2.5
|
78 |
+
00:00:04,419 --> 00:00:04,599
|
79 |
+
game
|
80 |
+
|
81 |
+
2.6
|
82 |
+
00:00:04,599 --> 00:00:04,879
|
83 |
+
plan.
|
84 |
+
|
85 |
+
3.1
|
86 |
+
00:00:05,559 --> 00:00:05,740
|
87 |
+
He
|
88 |
+
|
89 |
+
3.2
|
90 |
+
00:00:05,740 --> 00:00:05,919
|
91 |
+
just
|
92 |
+
|
93 |
+
3.3
|
94 |
+
00:00:05,919 --> 00:00:06,400
|
95 |
+
hasn't
|
96 |
+
|
97 |
+
3.4
|
98 |
+
00:00:06,400 --> 00:00:06,740
|
99 |
+
shared
|
100 |
+
|
101 |
+
3.5
|
102 |
+
00:00:06,740 --> 00:00:06,919
|
103 |
+
it
|
104 |
+
|
105 |
+
3.6
|
106 |
+
00:00:06,919 --> 00:00:07,040
|
107 |
+
with
|
108 |
+
|
109 |
+
3.7
|
110 |
+
00:00:07,040 --> 00:00:07,320
|
111 |
+
me.
|
112 |
+
|
113 |
+
4.1
|
114 |
+
00:00:07,860 --> 00:00:08,039
|
115 |
+
But
|
116 |
+
|
117 |
+
4.2
|
118 |
+
00:00:08,039 --> 00:00:08,160
|
119 |
+
I
|
120 |
+
|
121 |
+
4.3
|
122 |
+
00:00:08,160 --> 00:00:08,339
|
123 |
+
tell
|
124 |
+
|
125 |
+
4.4
|
126 |
+
00:00:08,339 --> 00:00:08,500
|
127 |
+
you
|
128 |
+
|
129 |
+
4.5
|
130 |
+
00:00:08,500 --> 00:00:08,619
|
131 |
+
what,
|
132 |
+
|
133 |
+
4.6
|
134 |
+
00:00:08,699 --> 00:00:08,779
|
135 |
+
I
|
136 |
+
|
137 |
+
4.7
|
138 |
+
00:00:08,779 --> 00:00:08,900
|
139 |
+
don't
|
140 |
+
|
141 |
+
4.8
|
142 |
+
00:00:08,900 --> 00:00:08,980
|
143 |
+
know
|
144 |
+
|
145 |
+
4.9
|
146 |
+
00:00:08,980 --> 00:00:09,140
|
147 |
+
about
|
148 |
+
|
149 |
+
4.10
|
150 |
+
00:00:09,140 --> 00:00:09,460
|
151 |
+
you,
|
152 |
+
|
153 |
+
4.11
|
154 |
+
00:00:09,839 --> 00:00:10,199
|
155 |
+
but
|
156 |
+
|
157 |
+
4.12
|
158 |
+
00:00:10,199 --> 00:00:10,419
|
159 |
+
I'm
|
160 |
+
|
161 |
+
4.13
|
162 |
+
00:00:10,419 --> 00:00:10,599
|
163 |
+
going
|
164 |
+
|
165 |
+
4.14
|
166 |
+
00:00:10,599 --> 00:00:10,720
|
167 |
+
to
|
168 |
+
|
169 |
+
4.15
|
170 |
+
00:00:10,720 --> 00:00:10,859
|
171 |
+
go
|
172 |
+
|
173 |
+
4.16
|
174 |
+
00:00:10,859 --> 00:00:11,000
|
175 |
+
to
|
176 |
+
|
177 |
+
4.17
|
178 |
+
00:00:11,000 --> 00:00:11,220
|
179 |
+
bed.
|
180 |
+
|
data/minecraft/audio.mp3
ADDED
Binary file (621 kB). View file
|
|
data/minecraft/subtitles.srt
ADDED
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
1.1
|
2 |
+
00:00:00,080 --> 00:00:00,220
|
3 |
+
This
|
4 |
+
|
5 |
+
1.2
|
6 |
+
00:00:00,220 --> 00:00:00,380
|
7 |
+
loop
|
8 |
+
|
9 |
+
1.3
|
10 |
+
00:00:00,380 --> 00:00:00,740
|
11 |
+
iterates
|
12 |
+
|
13 |
+
1.4
|
14 |
+
00:00:00,740 --> 00:00:00,920
|
15 |
+
over
|
16 |
+
|
17 |
+
1.5
|
18 |
+
00:00:00,920 --> 00:00:01,080
|
19 |
+
the
|
20 |
+
|
21 |
+
1.6
|
22 |
+
00:00:01,080 --> 00:00:01,260
|
23 |
+
range
|
24 |
+
|
25 |
+
1.7
|
26 |
+
00:00:01,260 --> 00:00:01,379
|
27 |
+
of
|
28 |
+
|
29 |
+
1.8
|
30 |
+
00:00:01,379 --> 00:00:01,500
|
31 |
+
the
|
32 |
+
|
33 |
+
1.9
|
34 |
+
00:00:01,500 --> 00:00:01,639
|
35 |
+
length
|
36 |
+
|
37 |
+
1.10
|
38 |
+
00:00:01,639 --> 00:00:01,780
|
39 |
+
of
|
40 |
+
|
41 |
+
1.11
|
42 |
+
00:00:01,780 --> 00:00:01,980
|
43 |
+
input
|
44 |
+
|
45 |
+
1.12
|
46 |
+
00:00:01,980 --> 00:00:02,240
|
47 |
+
clips
|
48 |
+
|
49 |
+
1.13
|
50 |
+
00:00:02,240 --> 00:00:02,439
|
51 |
+
and
|
52 |
+
|
53 |
+
1.14
|
54 |
+
00:00:02,439 --> 00:00:02,700
|
55 |
+
attempts
|
56 |
+
|
57 |
+
1.15
|
58 |
+
00:00:02,700 --> 00:00:02,919
|
59 |
+
to
|
60 |
+
|
61 |
+
1.16
|
62 |
+
00:00:02,919 --> 00:00:03,180
|
63 |
+
access
|
64 |
+
|
65 |
+
1.17
|
66 |
+
00:00:03,180 --> 00:00:03,439
|
67 |
+
each
|
68 |
+
|
69 |
+
1.18
|
70 |
+
00:00:03,439 --> 00:00:03,640
|
71 |
+
item
|
72 |
+
|
73 |
+
1.19
|
74 |
+
00:00:03,640 --> 00:00:03,819
|
75 |
+
and
|
76 |
+
|
77 |
+
1.20
|
78 |
+
00:00:03,819 --> 00:00:04,019
|
79 |
+
input
|
80 |
+
|
81 |
+
1.21
|
82 |
+
00:00:04,019 --> 00:00:04,419
|
83 |
+
underscore
|
84 |
+
|
85 |
+
1.22
|
86 |
+
00:00:04,419 --> 00:00:04,660
|
87 |
+
clips
|
88 |
+
|
89 |
+
1.23
|
90 |
+
00:00:04,660 --> 00:00:04,919
|
91 |
+
using
|
92 |
+
|
93 |
+
1.24
|
94 |
+
00:00:04,919 --> 00:00:05,240
|
95 |
+
input
|
96 |
+
|
97 |
+
1.25
|
98 |
+
00:00:05,240 --> 00:00:05,559
|
99 |
+
underscore
|
100 |
+
|
101 |
+
1.26
|
102 |
+
00:00:05,559 --> 00:00:05,879
|
103 |
+
clips
|
104 |
+
|
105 |
+
1.27
|
106 |
+
00:00:05,879 --> 00:00:06,500
|
107 |
+
IDX.
|
108 |
+
|
109 |
+
2.1
|
110 |
+
00:00:07,240 --> 00:00:07,400
|
111 |
+
The
|
112 |
+
|
113 |
+
2.2
|
114 |
+
00:00:07,400 --> 00:00:07,580
|
115 |
+
error
|
116 |
+
|
117 |
+
2.3
|
118 |
+
00:00:07,580 --> 00:00:07,919
|
119 |
+
suggests
|
120 |
+
|
121 |
+
2.4
|
122 |
+
00:00:07,919 --> 00:00:08,220
|
123 |
+
that
|
124 |
+
|
125 |
+
2.5
|
126 |
+
00:00:08,220 --> 00:00:08,699
|
127 |
+
IDX
|
128 |
+
|
129 |
+
2.6
|
130 |
+
00:00:08,699 --> 00:00:08,900
|
131 |
+
goes
|
132 |
+
|
133 |
+
2.7
|
134 |
+
00:00:08,900 --> 00:00:09,199
|
135 |
+
beyond
|
136 |
+
|
137 |
+
2.8
|
138 |
+
00:00:09,199 --> 00:00:09,339
|
139 |
+
the
|
140 |
+
|
141 |
+
2.9
|
142 |
+
00:00:09,339 --> 00:00:09,519
|
143 |
+
length
|
144 |
+
|
145 |
+
2.10
|
146 |
+
00:00:09,519 --> 00:00:09,660
|
147 |
+
of
|
148 |
+
|
149 |
+
2.11
|
150 |
+
00:00:09,660 --> 00:00:09,859
|
151 |
+
input
|
152 |
+
|
153 |
+
2.12
|
154 |
+
00:00:09,859 --> 00:00:10,199
|
155 |
+
clips,
|
156 |
+
|
157 |
+
2.13
|
158 |
+
00:00:10,560 --> 00:00:10,919
|
159 |
+
indicating
|
160 |
+
|
161 |
+
2.14
|
162 |
+
00:00:10,919 --> 00:00:11,179
|
163 |
+
that
|
164 |
+
|
165 |
+
2.15
|
166 |
+
00:00:11,179 --> 00:00:11,300
|
167 |
+
there
|
168 |
+
|
169 |
+
2.16
|
170 |
+
00:00:11,300 --> 00:00:11,460
|
171 |
+
might
|
172 |
+
|
173 |
+
2.17
|
174 |
+
00:00:11,460 --> 00:00:11,619
|
175 |
+
be
|
176 |
+
|
177 |
+
2.18
|
178 |
+
00:00:11,619 --> 00:00:11,820
|
179 |
+
fewer
|
180 |
+
|
181 |
+
2.19
|
182 |
+
00:00:11,820 --> 00:00:12,080
|
183 |
+
items
|
184 |
+
|
185 |
+
2.20
|
186 |
+
00:00:12,080 --> 00:00:12,300
|
187 |
+
in
|
188 |
+
|
189 |
+
2.21
|
190 |
+
00:00:12,300 --> 00:00:12,519
|
191 |
+
input
|
192 |
+
|
193 |
+
2.22
|
194 |
+
00:00:12,519 --> 00:00:12,759
|
195 |
+
clips
|
196 |
+
|
197 |
+
2.23
|
198 |
+
00:00:12,759 --> 00:00:12,960
|
199 |
+
than
|
200 |
+
|
201 |
+
2.24
|
202 |
+
00:00:12,960 --> 00:00:13,320
|
203 |
+
expected.
|
204 |
+
|
data/subway/audio.mp3
ADDED
Binary file (236 kB). View file
|
|
data/subway/subtitles.srt
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
1.1
|
2 |
+
00:00:00,000 --> 00:00:01,459
|
3 |
+
big
|
4 |
+
|
5 |
+
1.2
|
6 |
+
00:00:01,459 --> 00:00:01,899
|
7 |
+
data,
|
8 |
+
|
9 |
+
1.3
|
10 |
+
00:00:02,819 --> 00:00:03,439
|
11 |
+
machine
|
12 |
+
|
13 |
+
1.4
|
14 |
+
00:00:03,439 --> 00:00:03,879
|
15 |
+
learning,
|
16 |
+
|
17 |
+
1.5
|
18 |
+
00:00:04,660 --> 00:00:05,320
|
19 |
+
blockchain,
|
20 |
+
|
21 |
+
1.6
|
22 |
+
00:00:05,879 --> 00:00:06,740
|
23 |
+
artificial
|
24 |
+
|
25 |
+
1.7
|
26 |
+
00:00:06,740 --> 00:00:07,400
|
27 |
+
intelligence,
|
28 |
+
|
29 |
+
1.8
|
30 |
+
00:00:07,719 --> 00:00:07,980
|
31 |
+
digital
|
32 |
+
|
33 |
+
1.9
|
34 |
+
00:00:07,980 --> 00:00:08,599
|
35 |
+
manufacturing,
|
36 |
+
|
37 |
+
1.10
|
38 |
+
00:00:09,119 --> 00:00:09,380
|
39 |
+
big
|
40 |
+
|
41 |
+
1.11
|
42 |
+
00:00:09,380 --> 00:00:09,660
|
43 |
+
data
|
44 |
+
|
45 |
+
2.1
|
46 |
+
00:00:09,679 --> 00:00:10,140
|
47 |
+
analysis,
|
48 |
+
|
49 |
+
2.2
|
50 |
+
00:00:10,640 --> 00:00:11,060
|
51 |
+
quantum
|
52 |
+
|
53 |
+
2.3
|
54 |
+
00:00:11,060 --> 00:00:11,720
|
55 |
+
communication
|
56 |
+
|
57 |
+
2.4
|
58 |
+
00:00:11,720 --> 00:00:12,740
|
59 |
+
and
|
60 |
+
|
61 |
+
2.5
|
62 |
+
00:00:12,740 --> 00:00:13,660
|
63 |
+
internet
|
64 |
+
|
65 |
+
2.6
|
66 |
+
00:00:13,660 --> 00:00:13,900
|
67 |
+
of
|
68 |
+
|
69 |
+
2.7
|
70 |
+
00:00:13,900 --> 00:00:14,179
|
71 |
+
things,
|
72 |
+
|
73 |
+
2.8
|
74 |
+
00:00:14,259 --> 00:00:14,419
|
75 |
+
of
|
76 |
+
|
77 |
+
2.9
|
78 |
+
00:00:14,419 --> 00:00:14,679
|
79 |
+
things.
|
80 |
+
|
endpoints/__init__.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter
|
2 |
+
|
3 |
+
from .subway import router as subway_router
|
4 |
+
from .minecraft import router as minecraft_router
|
5 |
+
from .basic import router as basic_router
|
6 |
+
from .rumble import router as rumble_router
|
7 |
+
from .video_editor_cut import router as video_editor
|
endpoints/basic.py
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import UploadFile, File, Form, BackgroundTasks
|
2 |
+
from fastapi.responses import FileResponse
|
3 |
+
from fastapi import APIRouter
|
4 |
+
from generate_basic_subtitles import generate_basic_subtitles
|
5 |
+
from endpoints.utils.remove_content_from_dir import remove_content_from_dir
|
6 |
+
import os
|
7 |
+
import shutil
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
|
11 |
+
@router.post("/basic")
|
12 |
+
async def create_basic_video(
|
13 |
+
video: UploadFile = File(...),
|
14 |
+
color: str = Form(...),
|
15 |
+
size: int = Form(...),
|
16 |
+
font: str = Form(...),
|
17 |
+
credit: str = Form(...),
|
18 |
+
credit_size: int = Form(...),
|
19 |
+
background_tasks: BackgroundTasks = BackgroundTasks()
|
20 |
+
):
|
21 |
+
# Save the videos
|
22 |
+
basic_video_path = "./data/basic/videos/video.mp4"
|
23 |
+
with open(basic_video_path, "wb") as buffer:
|
24 |
+
shutil.copyfileobj(video.file, buffer)
|
25 |
+
|
26 |
+
options = {
|
27 |
+
"video": basic_video_path,
|
28 |
+
"font_color": color,
|
29 |
+
"font_size": size,
|
30 |
+
"font_family": font,
|
31 |
+
"border_size": 1,
|
32 |
+
"credit": credit,
|
33 |
+
"credit_size": credit_size,
|
34 |
+
}
|
35 |
+
|
36 |
+
result_video_path = generate_basic_subtitles(options)
|
37 |
+
|
38 |
+
|
39 |
+
if not os.path.exists(result_video_path):
|
40 |
+
return {"message": "Error generating subtitles!"}
|
41 |
+
|
42 |
+
background_tasks.add_task(remove_content_from_dir, "./data/basic/videos")
|
43 |
+
|
44 |
+
return FileResponse(result_video_path, media_type="video/mp4", filename="result.mp4")
|
endpoints/minecraft.py
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import UploadFile, File, Form, BackgroundTasks
|
2 |
+
from fastapi.responses import FileResponse
|
3 |
+
from fastapi import APIRouter
|
4 |
+
from generate_minecraft_subtitles import generate_minecraft_subtitles
|
5 |
+
from endpoints.utils.remove_content_from_dir import remove_content_from_dir
|
6 |
+
import os
|
7 |
+
import shutil
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
|
11 |
+
@router.post("/minecraft")
|
12 |
+
async def create_minecraft_video(
|
13 |
+
video: UploadFile = File(...),
|
14 |
+
subtitles: str = Form(...),
|
15 |
+
color: str = Form(...),
|
16 |
+
size: int = Form(...),
|
17 |
+
font: str = Form(...),
|
18 |
+
credit: str = Form(...),
|
19 |
+
credit_size: int = Form(...),
|
20 |
+
background_tasks: BackgroundTasks = BackgroundTasks()
|
21 |
+
):
|
22 |
+
|
23 |
+
background_video_path = "./data/minecraft/videos/background_video.mp4"
|
24 |
+
# Save the videos
|
25 |
+
with open(background_video_path, "wb") as buffer:
|
26 |
+
shutil.copyfileobj(video.file, buffer)
|
27 |
+
|
28 |
+
options = {
|
29 |
+
"background_video": background_video_path,
|
30 |
+
"font_color": color,
|
31 |
+
"font_size": size,
|
32 |
+
"font_family": font,
|
33 |
+
"border_size": 1,
|
34 |
+
"credit": credit,
|
35 |
+
"credit_size": credit_size,
|
36 |
+
}
|
37 |
+
|
38 |
+
result_video_path = generate_minecraft_subtitles(options, subtitles)
|
39 |
+
|
40 |
+
if not os.path.exists(result_video_path):
|
41 |
+
return {"message": "Error generating subtitles!"}
|
42 |
+
|
43 |
+
background_tasks.add_task(remove_content_from_dir, "./data/minecraft/videos")
|
44 |
+
|
45 |
+
return FileResponse(result_video_path, media_type="video/mp4", filename="result.mp4")
|
endpoints/subway.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import UploadFile, File, Form, BackgroundTasks
|
2 |
+
from fastapi.responses import FileResponse
|
3 |
+
from fastapi import APIRouter
|
4 |
+
from generate_subway_subtitles import generate_subway_subtitles
|
5 |
+
from endpoints.utils.remove_content_from_dir import remove_content_from_dir
|
6 |
+
import os
|
7 |
+
import shutil
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
|
11 |
+
@router.post("/subway")
|
12 |
+
async def create_subway_video(
|
13 |
+
top_video: UploadFile = File(...),
|
14 |
+
bottom_video: UploadFile = File(...),
|
15 |
+
color: str = Form(...),
|
16 |
+
size: int = Form(...),
|
17 |
+
font: str = Form(...),
|
18 |
+
credit: str = Form(...),
|
19 |
+
credit_size: int = Form(...),
|
20 |
+
background_tasks: BackgroundTasks = BackgroundTasks()
|
21 |
+
):
|
22 |
+
top_video_path = "./data/subway/videos/top_video.mp4"
|
23 |
+
bottom_video_path = "./data/subway/videos/bottom_video.mp4"
|
24 |
+
# Save the videos
|
25 |
+
with open(top_video_path, "wb") as buffer:
|
26 |
+
shutil.copyfileobj(top_video.file, buffer)
|
27 |
+
with open(bottom_video_path, "wb") as buffer:
|
28 |
+
shutil.copyfileobj(bottom_video.file, buffer)
|
29 |
+
|
30 |
+
options = {
|
31 |
+
"top_video": top_video_path,
|
32 |
+
"bottom_video": bottom_video_path,
|
33 |
+
"font_color": color,
|
34 |
+
"font_size": size,
|
35 |
+
"font_family": font,
|
36 |
+
"border_size": 1,
|
37 |
+
"credit": credit,
|
38 |
+
"credit_size": credit_size,
|
39 |
+
}
|
40 |
+
|
41 |
+
result_video_path = generate_subway_subtitles(options)
|
42 |
+
|
43 |
+
if not os.path.exists(result_video_path):
|
44 |
+
return {"message": "Error generating subtitles!"}
|
45 |
+
|
46 |
+
background_tasks.add_task(remove_content_from_dir, "./data/subway/videos")
|
47 |
+
|
48 |
+
return FileResponse(result_video_path, media_type="video/mp4", filename="result.mp4")
|
endpoints/utils/remove_content_from_dir.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import shutil
|
3 |
+
|
4 |
+
def remove_content_from_dir(folder):
|
5 |
+
for filename in os.listdir(folder):
|
6 |
+
file_path = os.path.join(folder, filename)
|
7 |
+
try:
|
8 |
+
if os.path.isfile(file_path) or os.path.islink(file_path):
|
9 |
+
os.unlink(file_path)
|
10 |
+
elif os.path.isdir(file_path):
|
11 |
+
shutil.rmtree(file_path)
|
12 |
+
except Exception as e:
|
13 |
+
print('Failed to delete %s. Reason: %s' % (file_path, e))
|
endpoints/video_editor_cut.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import shutil
|
3 |
+
import subprocess
|
4 |
+
from typing import List
|
5 |
+
from fastapi import UploadFile, File, Form
|
6 |
+
from fastapi.responses import FileResponse
|
7 |
+
from fastapi import APIRouter, BackgroundTasks
|
8 |
+
import os
|
9 |
+
|
10 |
+
router = APIRouter()
|
11 |
+
|
12 |
+
@router.post("/cut")
|
13 |
+
async def cut_videos(
|
14 |
+
videos: List[UploadFile] = File(...),
|
15 |
+
timestamps: str = Form(...),
|
16 |
+
background_tasks: BackgroundTasks = BackgroundTasks()
|
17 |
+
):
|
18 |
+
timestamps_dict = json.loads(timestamps)
|
19 |
+
temp_dir = "./data/video_editor/videos"
|
20 |
+
if not os.path.exists(temp_dir):
|
21 |
+
os.makedirs(temp_dir)
|
22 |
+
|
23 |
+
save_and_cut(videos, temp_dir, timestamps_dict)
|
24 |
+
|
25 |
+
clean_temp_dir = remove_temp_files(temp_dir)
|
26 |
+
|
27 |
+
zip_filename = shutil.make_archive(f"{clean_temp_dir}", 'zip', "./data/video_editor/")
|
28 |
+
|
29 |
+
background_tasks.add_task(remove_zip_folder, zip_filename)
|
30 |
+
|
31 |
+
return FileResponse(zip_filename)
|
32 |
+
|
33 |
+
|
34 |
+
def save_and_cut(videos, temp_dir, timestamps_dict):
|
35 |
+
for index, video in enumerate(videos):
|
36 |
+
temp_video_path = f"{temp_dir}/{video.filename}-{index}.mp4"
|
37 |
+
with open(temp_video_path, "wb") as f:
|
38 |
+
shutil.copyfileobj(video.file, f)
|
39 |
+
|
40 |
+
cut_start = timestamps_dict[str(index)]["start"]
|
41 |
+
cut_end = timestamps_dict[str(index)]["end"]
|
42 |
+
|
43 |
+
# Remove the "-index" from the filename by removing everything after the last "-"
|
44 |
+
filename_without_extension = os.path.splitext(temp_video_path)[0]
|
45 |
+
filename_without_index = filename_without_extension.rsplit("-", 1)[0]
|
46 |
+
output_video_path = f"{filename_without_index}_cut.mp4"
|
47 |
+
|
48 |
+
subprocess.run([
|
49 |
+
"ffmpeg",
|
50 |
+
"-i", temp_video_path,
|
51 |
+
"-ss", cut_start,
|
52 |
+
"-t", cut_end,
|
53 |
+
"-y", output_video_path
|
54 |
+
])
|
55 |
+
|
56 |
+
# Remove the temporary video file
|
57 |
+
os.remove(temp_video_path)
|
58 |
+
|
59 |
+
def remove_temp_files(temp_dir):
|
60 |
+
for filename in os.listdir(temp_dir):
|
61 |
+
if "cut" not in filename:
|
62 |
+
os.remove(f"{temp_dir}/{filename}")
|
63 |
+
if "cut" in filename:
|
64 |
+
os.rename(f"{temp_dir}/{filename}", f"{temp_dir}/{filename.replace('_cut.mp4', '')}")
|
65 |
+
return temp_dir
|
66 |
+
|
67 |
+
def remove_zip_folder(zip_filename):
|
68 |
+
if os.path.exists(zip_filename):
|
69 |
+
shutil.rmtree(zip_filename[:-4])
|
70 |
+
os.remove(zip_filename)
|
generate_basic_subtitles.py
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from utils.create_subtitles import hex_to_ffmpeg_color, create_subtitles
|
2 |
+
from utils.transcribe_audio import transcribe_audio
|
3 |
+
from utils.video_to_audio import extract_audio
|
4 |
+
import subprocess
|
5 |
+
import os
|
6 |
+
|
7 |
+
def generate_basic_subtitles(customization_options):
|
8 |
+
customization_options['font_color'] = hex_to_ffmpeg_color(customization_options['font_color'])
|
9 |
+
|
10 |
+
video_path = customization_options['video']
|
11 |
+
audio_file_path = "./data/basic/audio.mp3"
|
12 |
+
subtitle_file = "./data/basic/subtitles.srt"
|
13 |
+
output_directory = "./data/basic/videos"
|
14 |
+
|
15 |
+
extract_audio(video_path, audio_file_path)
|
16 |
+
transcription = transcribe_audio(audio_file_path)
|
17 |
+
create_subtitles(transcription, subtitle_file)
|
18 |
+
|
19 |
+
subtitle_output_path = add_subtitle(video_path, subtitle_file, customization_options, output_directory)
|
20 |
+
result_output_path = add_text_to_video(subtitle_output_path, customization_options, output_directory)
|
21 |
+
|
22 |
+
return result_output_path
|
23 |
+
|
24 |
+
|
25 |
+
def add_subtitle(video_path, subtitle_file, customization_options, output_directory):
|
26 |
+
subtitle_video_path = os.path.join(output_directory, 'subtitle_video.mp4')
|
27 |
+
|
28 |
+
# Add the subtitle to the video
|
29 |
+
subtitle_cmd = [
|
30 |
+
"ffmpeg",
|
31 |
+
"-i", video_path,
|
32 |
+
"-vf", f"subtitles={subtitle_file}:force_style='Fontsize={customization_options['font_size']},PrimaryColour={customization_options['font_color']},Fontname={customization_options['font_family']},MarginV=50'",
|
33 |
+
"-c:a", "copy",
|
34 |
+
"-y", subtitle_video_path
|
35 |
+
]
|
36 |
+
|
37 |
+
subprocess.run(subtitle_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
38 |
+
|
39 |
+
return subtitle_video_path
|
40 |
+
|
41 |
+
|
42 |
+
def add_text_to_video(subtitle_output_path, customization_options, output_directory):
|
43 |
+
result_video_path = os.path.join(output_directory, 'result.mp4')
|
44 |
+
cmd = [
|
45 |
+
'ffmpeg',
|
46 |
+
'-i', subtitle_output_path,
|
47 |
+
'-vf', f'drawtext=text=\'{customization_options["credit"]}\':fontcolor=white:fontsize={customization_options["credit_size"]}:box=1:[email protected]:boxborderw=5:x=(w-text_w)/2:y=h-th-10',
|
48 |
+
'-codec:a', 'copy',
|
49 |
+
"-y", result_video_path
|
50 |
+
]
|
51 |
+
|
52 |
+
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
53 |
+
|
54 |
+
return result_video_path
|
generate_minecraft_subtitles.py
ADDED
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from utils.create_subtitles import hex_to_ffmpeg_color, create_subtitles
|
2 |
+
from utils.transcribe_audio import transcribe_audio
|
3 |
+
from utils.text_to_speech import text_to_speech
|
4 |
+
import subprocess
|
5 |
+
import os
|
6 |
+
|
7 |
+
def generate_minecraft_subtitles(customization_options, subtitles):
|
8 |
+
customization_options['font_color'] = hex_to_ffmpeg_color(customization_options['font_color'])
|
9 |
+
|
10 |
+
audio_file_path = "./data/minecraft/audio.mp3"
|
11 |
+
subtitle_file = "./data/minecraft/subtitles.srt"
|
12 |
+
|
13 |
+
background_video_path = customization_options['background_video']
|
14 |
+
output_directory = "./data/minecraft/videos"
|
15 |
+
|
16 |
+
text_to_speech(subtitles, audio_file_path)
|
17 |
+
transcription = transcribe_audio(audio_file_path)
|
18 |
+
create_subtitles(transcription, subtitle_file)
|
19 |
+
|
20 |
+
audio_output_video_path = background_video_operations(background_video_path, audio_file_path, output_directory)
|
21 |
+
subtitle_output_path = add_subtitle(audio_output_video_path, subtitle_file, customization_options, output_directory)
|
22 |
+
result_output_path = add_text_to_video(subtitle_output_path, customization_options, output_directory)
|
23 |
+
|
24 |
+
return result_output_path
|
25 |
+
|
26 |
+
|
27 |
+
|
28 |
+
def background_video_operations(background_video_path, audio_file_path, output_directory):
|
29 |
+
muted_output_filepath = os.path.join(output_directory, 'video_muted.mp4')
|
30 |
+
trimmed_output_filepath = os.path.join(output_directory, 'video_muted_trimmed.mp4')
|
31 |
+
audio_output_video_path = os.path.join(output_directory, 'audio_background_video.mp4')
|
32 |
+
|
33 |
+
# Mute the background video
|
34 |
+
mute_background_cmd = [
|
35 |
+
"ffmpeg",
|
36 |
+
"-i", background_video_path,
|
37 |
+
"-af", "volume=0.0",
|
38 |
+
"-c:v", "copy",
|
39 |
+
"-y", muted_output_filepath
|
40 |
+
]
|
41 |
+
|
42 |
+
subprocess.run(mute_background_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
43 |
+
|
44 |
+
|
45 |
+
# Audio duration
|
46 |
+
audio_duration_cmd = [
|
47 |
+
'ffprobe',
|
48 |
+
'-i', audio_file_path,
|
49 |
+
'-show_entries',
|
50 |
+
'format=duration',
|
51 |
+
'-v', 'quiet',
|
52 |
+
'-of', 'csv=p=0'
|
53 |
+
]
|
54 |
+
audio_duration = float(subprocess.check_output(audio_duration_cmd).decode('utf-8').strip())
|
55 |
+
|
56 |
+
# Trim the video to match the duration of the audio
|
57 |
+
trim_video_cmd = [
|
58 |
+
'ffmpeg',
|
59 |
+
'-i', muted_output_filepath,
|
60 |
+
'-t', str(audio_duration),
|
61 |
+
'-c', 'copy',
|
62 |
+
"-y", trimmed_output_filepath
|
63 |
+
]
|
64 |
+
subprocess.run(trim_video_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
65 |
+
|
66 |
+
# Place the audio over the video
|
67 |
+
add_audio_cmd = [
|
68 |
+
'ffmpeg',
|
69 |
+
'-i', trimmed_output_filepath,
|
70 |
+
'-i', audio_file_path,
|
71 |
+
'-c:v', 'copy',
|
72 |
+
'-c:a', 'aac',
|
73 |
+
'-map', '0:v:0',
|
74 |
+
'-map', '1:a:0',
|
75 |
+
'-shortest',
|
76 |
+
'-y', audio_output_video_path
|
77 |
+
]
|
78 |
+
|
79 |
+
subprocess.run(add_audio_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
80 |
+
|
81 |
+
return audio_output_video_path
|
82 |
+
|
83 |
+
|
84 |
+
def add_subtitle(audio_output_video_path, subtitle_file, customization_options, output_directory):
|
85 |
+
subtitle_video_path = os.path.join(output_directory, 'subtitle_video.mp4')
|
86 |
+
|
87 |
+
subtitle_cmd = [
|
88 |
+
"ffmpeg",
|
89 |
+
"-i", audio_output_video_path,
|
90 |
+
"-vf", f"subtitles={subtitle_file}:force_style='Fontsize={customization_options['font_size']},Fontname={customization_options['font_family']},BorderColor=black@{customization_options['border_size']},PrimaryColour={customization_options['font_color']}'",
|
91 |
+
"-c:a", "aac",
|
92 |
+
"-c:v", "libx264",
|
93 |
+
"-y", subtitle_video_path,
|
94 |
+
]
|
95 |
+
|
96 |
+
subprocess.run(subtitle_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
97 |
+
|
98 |
+
return subtitle_video_path
|
99 |
+
|
100 |
+
|
101 |
+
def add_text_to_video(subtitle_output_path, customization_options, output_directory):
|
102 |
+
result_video_path = os.path.join(output_directory, 'result.mp4')
|
103 |
+
cmd = [
|
104 |
+
'ffmpeg',
|
105 |
+
'-i', subtitle_output_path,
|
106 |
+
'-vf', f'drawtext=text=\'{customization_options["credit"]}\':fontcolor=white:fontsize={customization_options["credit_size"]}:box=1:[email protected]:boxborderw=5:x=(w-text_w)/2:y=(h-text_h)/2',
|
107 |
+
'-codec:a', 'copy',
|
108 |
+
"-y", result_video_path
|
109 |
+
]
|
110 |
+
|
111 |
+
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
112 |
+
|
113 |
+
return result_video_path
|
generate_subway_subtitles.py
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import subprocess
|
2 |
+
from utils.video_to_audio import extract_audio
|
3 |
+
from utils.transcribe_audio import transcribe_audio
|
4 |
+
from utils.create_subtitles import create_subtitles, hex_to_ffmpeg_color
|
5 |
+
import os
|
6 |
+
|
7 |
+
|
8 |
+
def generate_subway_subtitles(customization_options):
|
9 |
+
customization_options['font_color'] = hex_to_ffmpeg_color(customization_options['font_color'])
|
10 |
+
audio_file = "./data/subway/audio.mp3"
|
11 |
+
subtitle_file = "./data/subway/subtitles.srt"
|
12 |
+
|
13 |
+
top_video_path = customization_options['top_video']
|
14 |
+
bottom_video_path = customization_options['bottom_video']
|
15 |
+
output_directory = "./data/subway/videos"
|
16 |
+
|
17 |
+
extract_audio(bottom_video_path, audio_file)
|
18 |
+
transcription = transcribe_audio(audio_file)
|
19 |
+
create_subtitles(transcription, subtitle_file)
|
20 |
+
|
21 |
+
trimmed_output_filepath = top_video_operations(top_video_path, bottom_video_path, output_directory)
|
22 |
+
|
23 |
+
bottom_video_width, bottom_video_height = bottom_video_operations(bottom_video_path)
|
24 |
+
|
25 |
+
combined_output_path = combination_video(trimmed_output_filepath, bottom_video_path, bottom_video_width, bottom_video_height, output_directory)
|
26 |
+
|
27 |
+
subtitle_output_filepath = add_subtitle(combined_output_path, subtitle_file, customization_options, output_directory)
|
28 |
+
|
29 |
+
result_video_path = add_text_to_video(subtitle_output_filepath, customization_options, output_directory)
|
30 |
+
|
31 |
+
return result_video_path
|
32 |
+
|
33 |
+
|
34 |
+
|
35 |
+
def top_video_operations(top_video_path, bottom_video_path, output_directory):
|
36 |
+
muted_output_filepath = os.path.join(output_directory, 'top_video_muted.mp4')
|
37 |
+
|
38 |
+
trimmed_output_filepath = os.path.join(output_directory, 'top_video_muted_trimmed.mp4')
|
39 |
+
|
40 |
+
mute_top_cmd = [
|
41 |
+
"ffmpeg",
|
42 |
+
"-i", top_video_path,
|
43 |
+
"-af", "volume=0.0",
|
44 |
+
"-c:v", "copy",
|
45 |
+
"-y", muted_output_filepath
|
46 |
+
]
|
47 |
+
subprocess.run(mute_top_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
48 |
+
|
49 |
+
get_duration_cmd = [
|
50 |
+
"ffprobe",
|
51 |
+
"-v", "error",
|
52 |
+
"-show_entries", "format=duration",
|
53 |
+
"-of", "default=noprint_wrappers=1:nokey=1",
|
54 |
+
bottom_video_path
|
55 |
+
]
|
56 |
+
duration = float(subprocess.check_output(get_duration_cmd, universal_newlines=True).strip())
|
57 |
+
|
58 |
+
trim_top_cmd = [
|
59 |
+
"ffmpeg",
|
60 |
+
"-i", muted_output_filepath,
|
61 |
+
"-t", str(duration),
|
62 |
+
"-c:v", "copy",
|
63 |
+
"-y", trimmed_output_filepath
|
64 |
+
]
|
65 |
+
|
66 |
+
subprocess.run(trim_top_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
67 |
+
|
68 |
+
return trimmed_output_filepath
|
69 |
+
|
70 |
+
|
71 |
+
def bottom_video_operations(bottom_video_path):
|
72 |
+
bottom_video_height = int(subprocess.check_output([
|
73 |
+
"ffprobe",
|
74 |
+
"-v", "error",
|
75 |
+
"-select_streams", "v:0",
|
76 |
+
"-show_entries", "stream=height",
|
77 |
+
"-of", "csv=s=x:p=0",
|
78 |
+
bottom_video_path
|
79 |
+
], universal_newlines=True).strip())
|
80 |
+
|
81 |
+
bottom_video_width = int(subprocess.check_output([
|
82 |
+
"ffprobe",
|
83 |
+
"-v", "error",
|
84 |
+
"-select_streams", "v:0",
|
85 |
+
"-show_entries", "stream=width",
|
86 |
+
"-of", "csv=s=x:p=0",
|
87 |
+
bottom_video_path
|
88 |
+
], universal_newlines=True).strip())
|
89 |
+
|
90 |
+
return bottom_video_width, bottom_video_height
|
91 |
+
|
92 |
+
def combination_video(trimmed_top_video_path, bottom_video_path, bottom_video_width, bottom_video_height, output_directory):
|
93 |
+
combined_output_filepath = os.path.join(output_directory, 'top_video_muted_trimmed_combined.mp4')
|
94 |
+
|
95 |
+
combine_cmd = [
|
96 |
+
"ffmpeg",
|
97 |
+
"-i", trimmed_top_video_path,
|
98 |
+
"-i", bottom_video_path,
|
99 |
+
"-filter_complex", f"[0:v]scale={bottom_video_width}:{bottom_video_height},setsar=1[main];[1:v]scale={bottom_video_width}:{bottom_video_height},setsar=1[bottom_scaled];[main][bottom_scaled]vstack=inputs=2[v];[0:a][1:a]amix=inputs=2[a]",
|
100 |
+
"-map", "[v]",
|
101 |
+
"-map", "[a]",
|
102 |
+
"-y", combined_output_filepath
|
103 |
+
]
|
104 |
+
|
105 |
+
subprocess.run(combine_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
106 |
+
|
107 |
+
return combined_output_filepath
|
108 |
+
|
109 |
+
|
110 |
+
def add_subtitle(combined_video_path, subtitle_file, customization_options, output_directory):
|
111 |
+
subtitle_output_filepath = os.path.join(output_directory, 'combined_subtitle.mp4')
|
112 |
+
|
113 |
+
subtitle_cmd = [
|
114 |
+
"ffmpeg",
|
115 |
+
"-i", combined_video_path,
|
116 |
+
"-vf", f"subtitles={subtitle_file}:force_style='Fontsize={customization_options['font_size']},Fontname={customization_options['font_family']},BorderColor=black@{customization_options['border_size']},PrimaryColour={customization_options['font_color']}'",
|
117 |
+
"-c:a", "aac",
|
118 |
+
"-c:v", "libx264",
|
119 |
+
"-y", subtitle_output_filepath,
|
120 |
+
]
|
121 |
+
|
122 |
+
subprocess.run(subtitle_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
123 |
+
|
124 |
+
return subtitle_output_filepath
|
125 |
+
|
126 |
+
def add_text_to_video(subtitle_output_filepath, customization_options, output_directory):
|
127 |
+
result_video_path = os.path.join(output_directory, 'combined_subtitle_text.mp4')
|
128 |
+
|
129 |
+
cmd = [
|
130 |
+
'ffmpeg',
|
131 |
+
'-i', subtitle_output_filepath,
|
132 |
+
'-vf', f'drawtext=text=\'{customization_options["credit"]}\':fontcolor=white:fontsize={customization_options["credit_size"]}:box=1:[email protected]:boxborderw=5:x=(w-text_w)/2:y=(h-text_h)/2',
|
133 |
+
'-codec:a', 'copy',
|
134 |
+
"-y", result_video_path
|
135 |
+
]
|
136 |
+
|
137 |
+
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
138 |
+
|
139 |
+
return result_video_path
|
140 |
+
|
requirements.txt
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
openai-whisper
|
2 |
+
moviepy
|
3 |
+
setuptools
|
4 |
+
torch
|
5 |
+
imageio["ffmpeg"]
|
6 |
+
uvicorn
|
7 |
+
fastapi
|
8 |
+
pydantic
|
9 |
+
python-multipart
|
10 |
+
whisper-timestamped
|
11 |
+
pyttsx3
|
12 |
+
requests
|
13 |
+
beautifulsoup4
|
14 |
+
httpx
|
server.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
3 |
+
import uvicorn
|
4 |
+
from endpoints import subway_router, minecraft_router, basic_router, rumble_router, video_editor
|
5 |
+
|
6 |
+
app = FastAPI()
|
7 |
+
|
8 |
+
# Add CORS middleware
|
9 |
+
app.add_middleware(
|
10 |
+
CORSMiddleware,
|
11 |
+
allow_origins=["*"],
|
12 |
+
allow_methods=["*"],
|
13 |
+
allow_headers=["*"]
|
14 |
+
)
|
15 |
+
|
16 |
+
app.include_router(subway_router, prefix="/generate-subtitles")
|
17 |
+
app.include_router(minecraft_router, prefix="/generate-subtitles")
|
18 |
+
app.include_router(basic_router, prefix="/generate-subtitles")
|
19 |
+
app.include_router(rumble_router, prefix="/generate-subtitles")
|
20 |
+
app.include_router(video_editor, prefix="/video-editor")
|
21 |
+
|
22 |
+
if __name__ == "__main__":
|
23 |
+
uvicorn.run("server:app", host="localhost", port=8000, reload=True)
|
utils/create_subtitles.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
def create_subtitles(transcription, subtitle_file_path):
|
2 |
+
def convert_to_srt_time(time):
|
3 |
+
milliseconds = int((time - int(time)) * 1000)
|
4 |
+
minutes, seconds = divmod(int(time), 60)
|
5 |
+
hours, minutes = divmod(minutes, 60)
|
6 |
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}"
|
7 |
+
|
8 |
+
|
9 |
+
with open(subtitle_file_path, "w") as subtitle_file:
|
10 |
+
for i, segments in enumerate(transcription['segments'], start=1):
|
11 |
+
for j, words in enumerate(segments['words'], start=1):
|
12 |
+
start_time = convert_to_srt_time(words['start'])
|
13 |
+
end_time = convert_to_srt_time(words['end'])
|
14 |
+
|
15 |
+
subtitle_line = f"{i}.{j}\n{start_time} --> {end_time}\n{words['text']}\n\n"
|
16 |
+
|
17 |
+
subtitle_file.write(subtitle_line)
|
18 |
+
|
19 |
+
def hex_to_ffmpeg_color(hex_color):
|
20 |
+
# Remove '#' if present
|
21 |
+
hex_color = hex_color.lstrip('#')
|
22 |
+
|
23 |
+
# Convert hex to FFmpeg color format
|
24 |
+
ffmpeg_color = int(hex_color, 16)
|
25 |
+
return f'0x{ffmpeg_color:06x}'
|
utils/text_to_speech.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pyttsx3
|
2 |
+
|
3 |
+
def text_to_speech(text, audio_file_path):
|
4 |
+
engine = pyttsx3.init()
|
5 |
+
engine.save_to_file(text, audio_file_path)
|
6 |
+
engine.runAndWait()
|
utils/transcribe_audio.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import whisper_timestamped as whisper
|
2 |
+
|
3 |
+
def transcribe_audio(audio_file):
|
4 |
+
audio = whisper.load_audio(audio_file)
|
5 |
+
model = whisper.load_model("base")
|
6 |
+
result = whisper.transcribe(model, audio)
|
7 |
+
return result
|
utils/video_to_audio.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from moviepy.editor import VideoFileClip
|
2 |
+
|
3 |
+
def extract_audio(input_video, output_audio):
|
4 |
+
try:
|
5 |
+
video_clip = VideoFileClip(input_video)
|
6 |
+
audio_clip = video_clip.audio
|
7 |
+
|
8 |
+
if audio_clip:
|
9 |
+
audio_clip.write_audiofile(output_audio)
|
10 |
+
|
11 |
+
video_clip.close()
|
12 |
+
audio_clip.close()
|
13 |
+
except Exception as e:
|
14 |
+
print(f"Error extracting audio: {e}")
|