marquesafonso
commited on
Commit
·
748e637
1
Parent(s):
9c8d073
added project files
Browse files- .dockerignore +8 -0
- .gitignore +166 -0
- Dockerfile +23 -0
- README.md +7 -1
- docker-compose.yml +9 -0
- main.py +121 -0
- requirements.txt +0 -0
- static/landing_page.html +157 -0
- static/submit_video.html +196 -0
- utils/__init__.py +0 -0
- utils/api_configs.py +6 -0
- utils/logger.py +22 -0
- utils/process_video.py +27 -0
- utils/read_html.py +4 -0
- utils/subtitler.py +59 -0
- utils/transcriber.py +19 -0
- utils/zip_response.py +9 -0
.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
|