marquesafonso commited on
Commit
748e637
·
1 Parent(s): 9c8d073

added project files

Browse files
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ .gitignore
2
+ .devcontainer
3
+ __pycache__/
4
+ *.git
5
+ temp/
6
+ archive/
7
+ Pipfile
8
+ Pipfile.lock
.gitignore ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ archive/*
2
+ temp/
3
+ api_config.yml
4
+
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ data/*
10
+ Python-3.11.6/
11
+
12
+ # C extensions
13
+ *.so
14
+
15
+ # Distribution / packaging
16
+ .Python
17
+ build/
18
+ develop-eggs/
19
+ dist/
20
+ downloads/
21
+ eggs/
22
+ .eggs/
23
+ lib/
24
+ lib64/
25
+ parts/
26
+ sdist/
27
+ var/
28
+ wheels/
29
+ share/python-wheels/
30
+ *.egg-info/
31
+ .installed.cfg
32
+ *.egg
33
+ MANIFEST
34
+
35
+ # PyInstaller
36
+ # Usually these files are written by a python script from a template
37
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
38
+ *.manifest
39
+ *.spec
40
+
41
+ # Installer logs
42
+ pip-log.txt
43
+ pip-delete-this-directory.txt
44
+
45
+ # Unit test / coverage reports
46
+ htmlcov/
47
+ .tox/
48
+ .nox/
49
+ .coverage
50
+ .coverage.*
51
+ .cache
52
+ nosetests.xml
53
+ coverage.xml
54
+ *.cover
55
+ *.py,cover
56
+ .hypothesis/
57
+ .pytest_cache/
58
+ cover/
59
+
60
+ # Translations
61
+ *.mo
62
+ *.pot
63
+
64
+ # Django stuff:
65
+ *.log
66
+ local_settings.py
67
+ db.sqlite3
68
+ db.sqlite3-journal
69
+
70
+ # Flask stuff:
71
+ instance/
72
+ .webassets-cache
73
+
74
+ # Scrapy stuff:
75
+ .scrapy
76
+
77
+ # Sphinx documentation
78
+ docs/_build/
79
+
80
+ # PyBuilder
81
+ .pybuilder/
82
+ target/
83
+
84
+ # Jupyter Notebook
85
+ .ipynb_checkpoints
86
+
87
+ # IPython
88
+ profile_default/
89
+ ipython_config.py
90
+
91
+ # pyenv
92
+ # For a library or package, you might want to ignore these files since the code is
93
+ # intended to run in multiple environments; otherwise, check them in:
94
+ # .python-version
95
+
96
+ # pipenv
97
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
98
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
99
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
100
+ # install all needed dependencies.
101
+ #Pipfile.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/#use-with-ide
116
+ .pdm.toml
117
+
118
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
119
+ __pypackages__/
120
+
121
+ # Celery stuff
122
+ celerybeat-schedule
123
+ celerybeat.pid
124
+
125
+ # SageMath parsed files
126
+ *.sage.py
127
+
128
+ # Environments
129
+ .env
130
+ .venv
131
+ env/
132
+ venv/
133
+ ENV/
134
+ env.bak/
135
+ venv.bak/
136
+
137
+ # Spyder project settings
138
+ .spyderproject
139
+ .spyproject
140
+
141
+ # Rope project settings
142
+ .ropeproject
143
+
144
+ # mkdocs documentation
145
+ /site
146
+
147
+ # mypy
148
+ .mypy_cache/
149
+ .dmypy.json
150
+ dmypy.json
151
+
152
+ # Pyre type checker
153
+ .pyre/
154
+
155
+ # pytype static type analyzer
156
+ .pytype/
157
+
158
+ # Cython debug symbols
159
+ cython_debug/
160
+
161
+ # PyCharm
162
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
163
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
164
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
165
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
166
+ #.idea/
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.11.7-slim-bullseye
3
+
4
+ RUN useradd -m -u 1000 user
5
+ USER user
6
+ ENV PATH="/home/user/.local/bin:$PATH"
7
+
8
+ # Set the working directory in the container to /app
9
+ WORKDIR /app
10
+
11
+ # Copy the current directory contents into the container at /app
12
+ COPY --chown=user . /app
13
+
14
+ #Install ImageMagick
15
+ RUN apt-get update && apt-get install -y imagemagick && sed -i '91d' /etc/ImageMagick-6/policy.xml
16
+ # Install any needed packages specified in requirements.txt
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Make port 8000 available to the world outside this container
20
+ EXPOSE 8000
21
+
22
+ # Run main.py when the container launches
23
+ CMD ["python", "main.py"]
README.md CHANGED
@@ -8,4 +8,10 @@ pinned: false
8
  license: cc-by-nc-4.0
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
8
  license: cc-by-nc-4.0
9
  ---
10
 
11
+ [//]: <> (Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference)
12
+
13
+ ## Multilang ASR Captioner
14
+
15
+ A multilingual automatic speech recognition and video captioning tool using faster whisper.
16
+
17
+ Supports real-time translation to english. Runs on consumer grade cpu.
docker-compose.yml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ app:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ ports:
7
+ - "8000:8000"
8
+ volumes:
9
+ - .:/app
main.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil, os, logging, uvicorn, tempfile
2
+ from typing import Optional
3
+
4
+ from utils.process_video import process_video
5
+ from utils.zip_response import zip_response
6
+ from utils.api_configs import api_configs
7
+ from utils.read_html import read_html
8
+ from utils.logger import setup_logger
9
+
10
+ from fastapi import FastAPI, UploadFile, HTTPException, Form, Depends
11
+ from fastapi.responses import HTMLResponse, Response
12
+ from fastapi.security import HTTPBasic
13
+ from pydantic import BaseModel, field_validator
14
+
15
+ app = FastAPI()
16
+ security = HTTPBasic()
17
+ api_configs_file = os.path.abspath("api_config.yml")
18
+
19
+ class MP4Video(BaseModel):
20
+ video_file: UploadFile
21
+
22
+ @property
23
+ def filename(self):
24
+ return self.video_file.filename
25
+ @property
26
+ def file(self):
27
+ return self.video_file.file
28
+
29
+ @field_validator('video_file')
30
+ def validate_video_file(cls, v):
31
+ if not v.filename.endswith('.mp4'):
32
+ raise HTTPException(status_code=500, detail='Invalid video file type. Please upload an MP4 file.')
33
+ return v
34
+
35
+ class SRTFile(BaseModel):
36
+ srt_file: Optional[UploadFile] = None
37
+
38
+ @property
39
+ def filename(self):
40
+ return self.srt_file.filename
41
+ @property
42
+ def file(self):
43
+ return self.srt_file.file
44
+ @property
45
+ def size(self):
46
+ return self.srt_file.size
47
+
48
+ @field_validator('srt_file')
49
+ def validate_srt_file(cls, v):
50
+ if v.size > 0 and not v.filename.endswith('.srt'):
51
+ raise HTTPException(status_code=422, detail='Invalid subtitle file type. Please upload an SRT file.')
52
+ return v
53
+
54
+ @app.get("/")
55
+ async def root():
56
+ html_content = f"""
57
+ {read_html(os.path.join(os.getcwd(),"static/landing_page.html"))}
58
+ """
59
+ return HTMLResponse(content=html_content)
60
+
61
+
62
+ @app.get("/submit_video/")
63
+ async def get_form():
64
+ html_content = f"""
65
+ {read_html(os.path.join(os.getcwd(),"static/submit_video.html"))}
66
+ """
67
+ return HTMLResponse(content=html_content)
68
+
69
+ async def get_temp_dir():
70
+ dir = tempfile.TemporaryDirectory()
71
+ try:
72
+ yield dir.name
73
+ finally:
74
+ del dir
75
+
76
+ @app.post("/process_video/")
77
+ async def process_video_api(video_file: MP4Video = Depends(),
78
+ srt_file: SRTFile = Depends(),
79
+ task: Optional[str] = Form("transcribe"),
80
+ max_words_per_line: Optional[int] = Form(6),
81
+ fontsize: Optional[int] = Form(42),
82
+ font: Optional[str] = Form("FuturaPTHeavy"),
83
+ bg_color: Optional[str] = Form("#070a13b3"),
84
+ text_color: Optional[str] = Form("white"),
85
+ caption_mode: Optional[str] = Form("desktop"),
86
+ temp_dir: str = Depends(get_temp_dir)
87
+ ):
88
+ try:
89
+ logging.info("Creating temporary directories")
90
+ with open(os.path.join(temp_dir, video_file.filename), 'w+b') as temp_file:
91
+ logging.info("Copying video UploadFile to the temporary directory")
92
+ try:
93
+ shutil.copyfileobj(video_file.file, temp_file)
94
+ finally:
95
+ video_file.file.close()
96
+ logging.info("Copying SRT UploadFile to the temp_input_path")
97
+ if srt_file.size > 0:
98
+ with open(os.path.join(temp_dir, f"{video_file.filename.split('.')[0]}.srt"), 'w+b') as temp_srt_file:
99
+ try:
100
+ shutil.copyfileobj(srt_file.file, temp_srt_file)
101
+ finally:
102
+ srt_file.file.close()
103
+ logging.info("Processing the video...")
104
+ output_path, _ = process_video(temp_file.name, temp_srt_file.name, task, max_words_per_line, fontsize, font, bg_color, text_color, caption_mode)
105
+ logging.info("Zipping response...")
106
+ with open(os.path.join(temp_dir, f"{video_file.filename.split('.')[0]}.zip"), 'w+b') as temp_zip_file:
107
+ zip_file = zip_response(temp_zip_file.name, [output_path, srt_path])
108
+ return Response(content = zip_file)
109
+ with open(os.path.join(temp_dir, f"{video_file.filename.split('.')[0]}.srt"), 'w+b') as temp_srt_file:
110
+ logging.info("Processing the video...")
111
+ output_path, srt_path = process_video(temp_file.name, None, task, max_words_per_line, fontsize, font, bg_color, text_color, caption_mode, api_configs_file)
112
+ logging.info("Zipping response...")
113
+ with open(os.path.join(temp_dir, f"{video_file.filename.split('.')[0]}.zip"), 'w+b') as temp_zip_file:
114
+ zip_file = zip_response(temp_zip_file.name, [output_path, srt_path])
115
+ return Response(content = zip_file)
116
+ except Exception as e:
117
+ raise HTTPException(status_code=500, detail=str(e))
118
+
119
+ if __name__ == "__main__":
120
+ app_logger = setup_logger('appLogger', 'main.log', level=logging.DEBUG)
121
+ uvicorn.run(app, host="0.0.0.0", port=8000)
requirements.txt ADDED
Binary file (2.86 kB). View file
 
static/landing_page.html ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" integrity="sha512-Fo3rlrZj/k7ujTnHg4CGR2D7kSs0v4LLanw2qksYuRlEzO+tcaEPQogQ0KaoGN26/zrn20ImR1DfuLWnOo7aBA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
5
+ <style>
6
+ /* CSS Styles */
7
+ body {
8
+ font-family: 'Arial', sans-serif;
9
+ background-color: #f0f0f0;
10
+ color: #333;
11
+ line-height: 1.6;
12
+ text-align: center;
13
+ padding-top: 50px;
14
+ margin: 0;
15
+ height: 100vh;
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ position: relative;
20
+ }
21
+
22
+ .container {
23
+ width: 90%;
24
+ max-width: 1000px; /* Increased max width */
25
+ margin: auto;
26
+ padding: 40px; /* Increased padding for more space */
27
+ background: #ffffff;
28
+ border-radius: 8px;
29
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); /* Enhanced shadowing */
30
+ transform: translateY(-35%);
31
+ }
32
+
33
+ h1 {
34
+ color: #333;
35
+ margin-bottom: 20px;
36
+ }
37
+
38
+ p {
39
+ font-size: 18px;
40
+ color: #666;
41
+ margin-bottom: 30px;
42
+ }
43
+
44
+ .button {
45
+ display: inline-block;
46
+ padding: 15px 30px; /* Increased padding for larger buttons */
47
+ margin: 10px;
48
+ border-radius: 4px; /* Slightly more rounded corners */
49
+ color: white;
50
+ background-color: #4CAF50;
51
+ text-decoration: none;
52
+ font-size: 18px;
53
+ transition: background-color 0.3s, box-shadow 0.3s;
54
+ }
55
+
56
+ .button:hover {
57
+ background-color: #45a049;
58
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); /* Subtle hover effect */
59
+ }
60
+
61
+ .button.docs {
62
+ background-color: #008CBA;
63
+ }
64
+
65
+ .button.docs:hover {
66
+ background-color: #007BAA;
67
+ }
68
+
69
+ .footer {
70
+ width: 100%;
71
+ background-color: #f0f0f0;
72
+ padding: 20px 0;
73
+ position: absolute;
74
+ bottom: 5rem;
75
+ text-align: center;
76
+ }
77
+
78
+ .footer a {
79
+ padding: 0.5rem;
80
+ text-decoration: none;
81
+ }
82
+ .fa-github:hover {
83
+ transform: scale(1.2)
84
+ }
85
+ .fa-github:hover {
86
+ transform: scale(1.2)
87
+ }
88
+ .fa-github{
89
+ color: #000000
90
+ }
91
+ .fa-linkedin:hover {
92
+ transform: scale(1.2)
93
+ }
94
+ .fa-linkedin {
95
+ color: #0077B5
96
+ }
97
+
98
+ /* Responsiveness */
99
+ @media (max-width: 768px) {
100
+ .container {
101
+ width: 95%;
102
+ padding: 20px;
103
+ display: flex; /* Added to create a flex container */
104
+ flex-direction: column; /* Stack elements vertically */
105
+ align-items: center; /* Center-align items for a neat look */
106
+ }
107
+
108
+ h1 {
109
+ font-size: 24px;
110
+ }
111
+
112
+ p {
113
+ font-size: 16px;
114
+ text-align: center; /* Center text for a balanced appearance */
115
+ }
116
+
117
+ .button {
118
+ width: 80%; /* Set a specific width for both buttons */
119
+ padding: 10px 20px;
120
+ font-size: 16px;
121
+ margin-bottom: 10px; /* Add some space between the buttons */
122
+ }
123
+
124
+ /* Ensure buttons are the same size */
125
+ .button.submit, .button.docs {
126
+ width: calc(80% - 20px); /* Adjusting width to account for padding */
127
+ }
128
+ }
129
+
130
+ @media (max-height: 500px) {
131
+ body {
132
+ padding-top: 20px;
133
+ height: auto;
134
+ }
135
+
136
+ .container {
137
+ align-items: center; /* Ensure center alignment in constrained height */
138
+ }
139
+ }
140
+ </style>
141
+ </head>
142
+ <body>
143
+ <div class="container">
144
+ <h1>Multilang-ASR-Captioner</h1>
145
+ <p>A multilingual automatic speech recognition and video captioning tool using faster whisper.</p>
146
+ <p>Supports real-time translation to english. Runs on consumer grade cpu.</p>
147
+ <a href="/submit_video" class="button submit">Submit Video</a>
148
+ <a href="/docs" class="button docs">Documentation</a>
149
+ </div>
150
+ <!-- Footer -->
151
+ <div class="footer">
152
+ <p>Created by:</p>
153
+ <a href="https://github.com/marquesafonso" class="github"><i class="fab fa-github fa-4x"></i></a>
154
+ <a href="https://www.linkedin.com/in/marquesafonso" class="linkedin"><i class="fab fa-linkedin fa-4x"></i></a>
155
+ </div>
156
+ </body>
157
+ </html>
static/submit_video.html ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" integrity="sha512-Fo3rlrZj/k7ujTnHg4CGR2D7kSs0v4LLanw2qksYuRlEzO+tcaEPQogQ0KaoGN26/zrn20ImR1DfuLWnOo7aBA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
5
+ <style>
6
+ /* CSS Styles */
7
+ body {
8
+ font-family: 'Arial', sans-serif;
9
+ background-color: #f0f0f0;
10
+ color: #333;
11
+ line-height: 1.6;
12
+ margin: 0;
13
+ padding: 0;
14
+ display: flex;
15
+ flex-direction: column;
16
+ min-height: 100vh;
17
+ }
18
+
19
+ form {
20
+ max-width: 900px;
21
+ margin: .9rem auto;
22
+ padding: 1rem;
23
+ background: #ffffff;
24
+ border-radius: 8px;
25
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
26
+ display: flex;
27
+ flex-direction: column;
28
+ }
29
+
30
+ .form-wrapper {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ gap: 20px;
34
+ }
35
+
36
+ .form-group {
37
+ flex: 1;
38
+ min-width: calc(50% - 20px);
39
+ box-sizing: border-box;
40
+ }
41
+
42
+ .form-group h3 {
43
+ margin-bottom: 15px;
44
+ color: #4CAF50;
45
+ font-size: 18px;
46
+ border-bottom: 2px solid #4CAF50;
47
+ padding-bottom: 5px;
48
+ }
49
+
50
+ input[type=file],
51
+ input[type=number],
52
+ input[type=text],
53
+ select {
54
+ width: 100%;
55
+ padding: 10px;
56
+ margin-bottom: 10px;
57
+ border-radius: 4px;
58
+ border: 1px solid #ddd;
59
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
60
+ font-size: 13px;
61
+ box-sizing: border-box; /* Ensure padding and border are included in the element's total width and height */
62
+ }
63
+
64
+ input[type=submit] {
65
+ width: 100%;
66
+ background-color: #4CAF50;
67
+ color: white;
68
+ padding: 12px 18px;
69
+ border: none;
70
+ border-radius: 5px;
71
+ cursor: pointer;
72
+ font-size: 15px;
73
+ margin-top: 20px;
74
+ transition: background-color 0.3s ease;
75
+ box-sizing: border-box;
76
+ }
77
+
78
+ input[type=submit]:hover {
79
+ background-color: #45a049;
80
+ }
81
+
82
+ label {
83
+ margin-top: 10px;
84
+ display: block;
85
+ font-weight: bold;
86
+ font-size: 13px;
87
+ }
88
+
89
+ .footer {
90
+ width: 100%;
91
+ background-color: #f0f0f0;
92
+ text-align: center;
93
+ }
94
+
95
+ .footer a {
96
+ padding: 0.5rem;
97
+ text-decoration: none;
98
+ }
99
+
100
+ .fa-github:hover {
101
+ transform: scale(1.2);
102
+ }
103
+
104
+ .fa-github {
105
+ color: #000000;
106
+ }
107
+
108
+ .fa-linkedin:hover {
109
+ transform: scale(1.2);
110
+ }
111
+
112
+ .fa-linkedin {
113
+ color: #0077B5;
114
+ }
115
+
116
+ /* Additional Responsiveness */
117
+ @media (max-width: 992px) {
118
+ form {
119
+ max-width: 90%;
120
+ margin-left: 15%;
121
+ margin-right: 15%;
122
+ padding: 15px;
123
+ }
124
+
125
+ .form-wrapper {
126
+ flex-direction: column;
127
+ }
128
+
129
+ .form-group {
130
+ min-width: 100%;
131
+ }
132
+ }
133
+
134
+ @media (max-width: 768px) {
135
+ form {
136
+ max-width: 90%;
137
+ margin-left: 10%;
138
+ margin-right: 10%;
139
+ padding: 15px;
140
+ }
141
+ }
142
+
143
+ @media (max-width: 480px) {
144
+ form {
145
+ max-width: 90%;
146
+ margin-left: 5%;
147
+ margin-right: 5%;
148
+ padding: 10px;
149
+ }
150
+ }
151
+ </style>
152
+ </head>
153
+ <body>
154
+ <form action="/process_video/" enctype="multipart/form-data" method="post">
155
+ <div class="form-wrapper">
156
+ <div class="form-group">
157
+ <h3>Inputs & Task Selection</h3>
158
+ <label for="video_file">Video File</label>
159
+ <input type="file" id="video_file" name="video_file"><br>
160
+ <label for="srt_file">Subtitles File</label>
161
+ <input type="file" id="srt_file" name="srt_file"><br>
162
+ <label for="task">Task</label>
163
+ <select id="task" name="task">
164
+ <option value="transcribe">Transcribe</option>
165
+ <option value="translate">Translate</option>
166
+ </select>
167
+ </div>
168
+ <div class="form-group">
169
+ <h3>Visual Parameters</h3>
170
+ <label for="max_words_per_line">Max words per line</label>
171
+ <input type="number" id="max_words_per_line" name="max_words_per_line" value="6"><br>
172
+ <label for="fontsize">Font size</label>
173
+ <input type="number" id="fontsize" name="fontsize" value="42"><br>
174
+ <label for="font">Font:</label>
175
+ <input type="text" id="font" name="font" value="FuturaPTHeavy"><br>
176
+ <label for="bg_color">Background color</label>
177
+ <input type="text" id="bg_color" name="bg_color" value="#00FFFF00"><br>
178
+ <label for="text_color">Text color</label>
179
+ <input type="text" id="text_color" name="text_color" value="white"><br>
180
+ <label for="caption_mode">Caption mode</label>
181
+ <select id="caption_mode" name="caption_mode">
182
+ <option value="desktop">Desktop</option>
183
+ <option value="mobile">Mobile</option>
184
+ </select>
185
+ </div>
186
+ </div>
187
+ <input type="submit" value="Submit">
188
+ </form>
189
+ <!-- Footer -->
190
+ <div class="footer">
191
+ <p>Created by:</p>
192
+ <a href="https://github.com/marquesafonso" class="github"><i class="fab fa-github fa-3x"></i></a>
193
+ <a href="https://www.linkedin.com/in/marquesafonso" class="linkedin"><i class="fab fa-linkedin fa-3x"></i></a>
194
+ </div>
195
+ </body>
196
+ </html>
utils/__init__.py ADDED
File without changes
utils/api_configs.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import yaml
2
+
3
+ def api_configs(config_file):
4
+ with open(config_file, 'r') as f:
5
+ db_config = yaml.safe_load(f)
6
+ return db_config["api_config"]
utils/logger.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ # https://signoz.io/guides/python-logging-best-practices/
4
+ # Create and configure a named logger
5
+ def setup_logger(name, log_file, level=logging.INFO):
6
+ logger = logging.getLogger(name)
7
+ logger.setLevel(level)
8
+
9
+ # Create handlers
10
+ file_handler = logging.FileHandler(log_file)
11
+ console_handler = logging.StreamHandler()
12
+
13
+ # Create formatters and add them to handlers
14
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
15
+ file_handler.setFormatter(formatter)
16
+ console_handler.setFormatter(formatter)
17
+
18
+ # Add handlers to the logger
19
+ logger.addHandler(file_handler)
20
+ logger.addHandler(console_handler)
21
+
22
+ return logger
utils/process_video.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging, os
2
+ from utils.transcriber import transcriber
3
+ from utils.subtitler import subtitler
4
+
5
+ def process_video(invideo_file: str,
6
+ srt_file: str | None,
7
+ task: str,
8
+ max_words_per_line:int,
9
+ fontsize:str,
10
+ font:str,
11
+ bg_color:str,
12
+ text_color:str,
13
+ caption_mode:str,
14
+ config_file:str
15
+ ):
16
+ invideo_path_parts = os.path.normpath(invideo_file).split(os.path.sep)
17
+ VIDEO_NAME = os.path.basename(invideo_file)
18
+ OUTVIDEO_PATH = os.path.join(os.path.normpath('/'.join(invideo_path_parts[:-1])), f"result_{VIDEO_NAME}")
19
+ if srt_file:
20
+ logging.info("Subtitling...")
21
+ subtitler(invideo_file, srt_file, OUTVIDEO_PATH, fontsize, font, bg_color, text_color, caption_mode)
22
+ else:
23
+ srt_file = os.path.normpath(f"{invideo_file.split('.')[0]}.srt")
24
+ transcriber(invideo_file, srt_file, max_words_per_line, task, config_file)
25
+ logging.info("Subtitling...")
26
+ subtitler(invideo_file, srt_file, OUTVIDEO_PATH, fontsize, font, bg_color, text_color, caption_mode)
27
+ return OUTVIDEO_PATH, srt_file
utils/read_html.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ def read_html(html_file):
2
+ with open(html_file, 'r', encoding='utf-8') as f:
3
+ content = f.read()
4
+ return content
utils/subtitler.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from moviepy.editor import VideoFileClip, CompositeVideoClip, TextClip
2
+ import os
3
+
4
+ def parse_srt(srt_file):
5
+ """Parse the SRT file and return a list of (start, end, text) for each subtitle."""
6
+ with open(srt_file, "r", encoding='utf-8') as file:
7
+ lines = file.readlines()
8
+ i = 0
9
+ subtitles = []
10
+ while i < len(lines):
11
+ if lines[i].strip().isdigit():
12
+ timing_str = lines[i+1].strip().split(" --> ")
13
+ start = timing_str[0]
14
+ end = timing_str[1]
15
+ text = lines[i+2].strip()
16
+ subtitles.append((start, end, text))
17
+ i += 4
18
+ else:
19
+ i += 1
20
+ return subtitles
21
+
22
+ def filter_caption_width(caption_mode:str):
23
+ if caption_mode == 'desktop':
24
+ caption_width_ratio = 0.5
25
+ caption_height_ratio = 0.8
26
+ elif caption_mode == 'mobile':
27
+ caption_width_ratio = 0.2
28
+ caption_height_ratio = 0.7
29
+ return caption_width_ratio, caption_height_ratio
30
+
31
+ def subtitler(video_file:str,
32
+ srt_path:str,
33
+ output_file:str,
34
+ fontsize:int,
35
+ font: str,
36
+ bg_color:str,
37
+ text_color:str,
38
+ caption_mode:str
39
+ ):
40
+ """Add subtitles from an SRT file to a video."""
41
+ video_file = os.path.abspath(video_file)
42
+ srt_path = os.path.abspath(srt_path)
43
+ output_file = os.path.abspath(output_file)
44
+ clip = VideoFileClip(filename=video_file, target_resolution=None)
45
+ subtitles = parse_srt(srt_path)
46
+ subtitle_clips = []
47
+ caption_width_ratio, caption_height_ratio = filter_caption_width(caption_mode)
48
+ for start, end, text in subtitles:
49
+ # Create TextClip with specified styling
50
+ # To get a list of possible color and font values run: print(TextClip.list("font"), '\n\n', TextClip.list("color"))
51
+ txt_clip = TextClip(text, fontsize=fontsize, color=text_color, font=font, method='caption',
52
+ bg_color=bg_color, align='center', size=(clip.w*caption_width_ratio, None))
53
+ txt_clip = txt_clip.set_position(('center', 'bottom')).set_duration(clip.duration).set_start(start).set_end(end)
54
+ subtitle_x_position = 'center'
55
+ subtitle_y_position = clip.h * caption_height_ratio
56
+ text_position = (subtitle_x_position, subtitle_y_position)
57
+ subtitle_clips.append(txt_clip.set_position(text_position))
58
+ video = CompositeVideoClip(size=None, clips=[clip] + subtitle_clips)
59
+ video.write_videofile(output_file, codec='libx264', audio_codec='aac')
utils/transcriber.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from gradio_client import Client, handle_file
2
+ from utils.api_configs import api_configs
3
+ import tempfile
4
+
5
+ def transcriber(invideo_file:str, srt_file:str,
6
+ max_words_per_line:int, task:str,
7
+ config_file:str):
8
+ HF_TOKEN = api_configs(config_file)["secrets"]["hf-token"]
9
+ HF_SPACE = api_configs(config_file)["secrets"]["hf-space"]
10
+ client = Client(HF_SPACE, hf_token=HF_TOKEN)
11
+ result = client.predict(
12
+ video_input=handle_file(invideo_file),
13
+ max_words_per_line=max_words_per_line,
14
+ task=task,
15
+ api_name="/predict"
16
+ )
17
+ with open(srt_file, "w", encoding='utf-8') as file:
18
+ file.write(result[0])
19
+ return srt_file
utils/zip_response.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import zipfile, os
2
+
3
+ def zip_response(temp_zip_file: str, files: list):
4
+ with zipfile.ZipFile(temp_zip_file, 'w') as zipf:
5
+ for file in files:
6
+ zipf.write(file, arcname=os.path.basename(file))
7
+ with open(temp_zip_file, 'rb') as zip_file:
8
+ zip_bytes = zip_file.read()
9
+ return zip_bytes