Commit
·
d0c0836
1
Parent(s):
2dcfc88
add height ratio for better caption mode. pydantic v2 field validation. add archiver to main.
Browse files- docker-compose.yml +0 -2
- main.py +12 -6
- static/submit_video.html +2 -2
- utils/archiver.py +1 -1
- utils/process_video.py +3 -3
- utils/subtitler.py +12 -9
docker-compose.yml
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
version: '3.8'
|
| 2 |
-
|
| 3 |
services:
|
| 4 |
app:
|
| 5 |
build:
|
|
|
|
|
|
|
|
|
|
| 1 |
services:
|
| 2 |
app:
|
| 3 |
build:
|
main.py
CHANGED
|
@@ -2,11 +2,12 @@ from fastapi import FastAPI, UploadFile, HTTPException, Form, Depends
|
|
| 2 |
from fastapi.responses import FileResponse, HTMLResponse
|
| 3 |
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
| 4 |
from typing import Optional
|
| 5 |
-
from pydantic import BaseModel,
|
| 6 |
from utils.process_video import process_video
|
| 7 |
from utils.zip_response import zip_response
|
| 8 |
from utils.api_configs import api_configs
|
| 9 |
from utils.read_html import read_html
|
|
|
|
| 10 |
import shutil, os, logging, uvicorn, secrets
|
| 11 |
|
| 12 |
#TODO: upgrade project dependencies for the soon to be released version of faster-whisper that supports distil-largev3
|
|
@@ -42,7 +43,7 @@ class MP4Video(BaseModel):
|
|
| 42 |
def file(self):
|
| 43 |
return self.video_file.file
|
| 44 |
|
| 45 |
-
@
|
| 46 |
def validate_video_file(cls, v):
|
| 47 |
if not v.filename.endswith('.mp4'):
|
| 48 |
raise HTTPException(status_code=500, detail='Invalid video file type. Please upload an MP4 file.')
|
|
@@ -61,7 +62,7 @@ class SRTFile(BaseModel):
|
|
| 61 |
def size(self):
|
| 62 |
return self.srt_file.size
|
| 63 |
|
| 64 |
-
@
|
| 65 |
def validate_srt_file(cls, v):
|
| 66 |
if v.size > 0 and not v.filename.endswith('.srt'):
|
| 67 |
raise HTTPException(status_code=422, detail='Invalid subtitle file type. Please upload an SRT file.')
|
|
@@ -92,11 +93,12 @@ async def process_video_api(video_file: MP4Video = Depends(),
|
|
| 92 |
font: Optional[str] = Form("FuturaPTHeavy"),
|
| 93 |
bg_color: Optional[str] = Form("#070a13b3"),
|
| 94 |
text_color: Optional[str] = Form("white"),
|
| 95 |
-
|
| 96 |
username: str = Depends(get_current_user)
|
| 97 |
):
|
| 98 |
try:
|
| 99 |
logging.info("Creating temporary directories")
|
|
|
|
| 100 |
temp_dir = os.path.join(os.getcwd(),"temp")
|
| 101 |
os.makedirs(temp_dir, exist_ok=True)
|
| 102 |
temp_vid_dir = os.path.join(temp_dir,video_file.filename.split('.')[0])
|
|
@@ -117,12 +119,12 @@ async def process_video_api(video_file: MP4Video = Depends(),
|
|
| 117 |
finally:
|
| 118 |
srt_file.file.close()
|
| 119 |
logging.info("Processing the video...")
|
| 120 |
-
output_path, _ = process_video(temp_input_path, SRT_PATH, task, max_words_per_line, fontsize, font, bg_color, text_color,
|
| 121 |
logging.info("Zipping response...")
|
| 122 |
zip_path = zip_response(os.path.join(temp_vid_dir,"archive.zip"), [output_path, SRT_PATH])
|
| 123 |
return FileResponse(zip_path, media_type='application/zip', filename=f"result_{video_file.filename.split('.')[0]}.zip")
|
| 124 |
logging.info("Processing the video...")
|
| 125 |
-
output_path, srt_path = process_video(temp_input_path, None, task, max_words_per_line, fontsize, font, bg_color, text_color,
|
| 126 |
logging.info("Zipping response...")
|
| 127 |
zip_path = zip_response(os.path.join(temp_vid_dir,"archive.zip"), [output_path, srt_path])
|
| 128 |
return FileResponse(zip_path, media_type='application/zip', filename=f"result_{video_file.filename.split('.')[0]}.zip")
|
|
@@ -132,4 +134,8 @@ async def process_video_api(video_file: MP4Video = Depends(),
|
|
| 132 |
|
| 133 |
if __name__ == "__main__":
|
| 134 |
# Use Uvicorn to run the application
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|
|
| 2 |
from fastapi.responses import FileResponse, HTMLResponse
|
| 3 |
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
| 4 |
from typing import Optional
|
| 5 |
+
from pydantic import BaseModel, field_validator
|
| 6 |
from utils.process_video import process_video
|
| 7 |
from utils.zip_response import zip_response
|
| 8 |
from utils.api_configs import api_configs
|
| 9 |
from utils.read_html import read_html
|
| 10 |
+
from utils.archiver import archiver
|
| 11 |
import shutil, os, logging, uvicorn, secrets
|
| 12 |
|
| 13 |
#TODO: upgrade project dependencies for the soon to be released version of faster-whisper that supports distil-largev3
|
|
|
|
| 43 |
def file(self):
|
| 44 |
return self.video_file.file
|
| 45 |
|
| 46 |
+
@field_validator('video_file')
|
| 47 |
def validate_video_file(cls, v):
|
| 48 |
if not v.filename.endswith('.mp4'):
|
| 49 |
raise HTTPException(status_code=500, detail='Invalid video file type. Please upload an MP4 file.')
|
|
|
|
| 62 |
def size(self):
|
| 63 |
return self.srt_file.size
|
| 64 |
|
| 65 |
+
@field_validator('srt_file')
|
| 66 |
def validate_srt_file(cls, v):
|
| 67 |
if v.size > 0 and not v.filename.endswith('.srt'):
|
| 68 |
raise HTTPException(status_code=422, detail='Invalid subtitle file type. Please upload an SRT file.')
|
|
|
|
| 93 |
font: Optional[str] = Form("FuturaPTHeavy"),
|
| 94 |
bg_color: Optional[str] = Form("#070a13b3"),
|
| 95 |
text_color: Optional[str] = Form("white"),
|
| 96 |
+
caption_mode: Optional[str] = Form("desktop"),
|
| 97 |
username: str = Depends(get_current_user)
|
| 98 |
):
|
| 99 |
try:
|
| 100 |
logging.info("Creating temporary directories")
|
| 101 |
+
print(caption_mode)
|
| 102 |
temp_dir = os.path.join(os.getcwd(),"temp")
|
| 103 |
os.makedirs(temp_dir, exist_ok=True)
|
| 104 |
temp_vid_dir = os.path.join(temp_dir,video_file.filename.split('.')[0])
|
|
|
|
| 119 |
finally:
|
| 120 |
srt_file.file.close()
|
| 121 |
logging.info("Processing the video...")
|
| 122 |
+
output_path, _ = process_video(temp_input_path, SRT_PATH, task, max_words_per_line, fontsize, font, bg_color, text_color, caption_mode)
|
| 123 |
logging.info("Zipping response...")
|
| 124 |
zip_path = zip_response(os.path.join(temp_vid_dir,"archive.zip"), [output_path, SRT_PATH])
|
| 125 |
return FileResponse(zip_path, media_type='application/zip', filename=f"result_{video_file.filename.split('.')[0]}.zip")
|
| 126 |
logging.info("Processing the video...")
|
| 127 |
+
output_path, srt_path = process_video(temp_input_path, None, task, max_words_per_line, fontsize, font, bg_color, text_color, caption_mode)
|
| 128 |
logging.info("Zipping response...")
|
| 129 |
zip_path = zip_response(os.path.join(temp_vid_dir,"archive.zip"), [output_path, srt_path])
|
| 130 |
return FileResponse(zip_path, media_type='application/zip', filename=f"result_{video_file.filename.split('.')[0]}.zip")
|
|
|
|
| 134 |
|
| 135 |
if __name__ == "__main__":
|
| 136 |
# Use Uvicorn to run the application
|
| 137 |
+
try:
|
| 138 |
+
archiver()
|
| 139 |
+
except FileNotFoundError:
|
| 140 |
+
pass
|
| 141 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
static/submit_video.html
CHANGED
|
@@ -177,8 +177,8 @@
|
|
| 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="
|
| 181 |
-
<select id="
|
| 182 |
<option value="desktop">Desktop</option>
|
| 183 |
<option value="mobile">Mobile</option>
|
| 184 |
</select>
|
|
|
|
| 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>
|
utils/archiver.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import shutil, os
|
| 2 |
from datetime import datetime
|
| 3 |
|
| 4 |
-
def archiver(timestamp:datetime):
|
| 5 |
TIME = f"{timestamp.year:4d}-{timestamp.month:02d}-{timestamp.day:02d}_{timestamp.hour:02d}-{timestamp.minute:02d}"
|
| 6 |
ARCHIVE = os.path.abspath(f"archive/{TIME}")
|
| 7 |
TEMP_DIR = os.path.abspath("temp/")
|
|
|
|
| 1 |
import shutil, os
|
| 2 |
from datetime import datetime
|
| 3 |
|
| 4 |
+
def archiver(timestamp:datetime=datetime.now()):
|
| 5 |
TIME = f"{timestamp.year:4d}-{timestamp.month:02d}-{timestamp.day:02d}_{timestamp.hour:02d}-{timestamp.minute:02d}"
|
| 6 |
ARCHIVE = os.path.abspath(f"archive/{TIME}")
|
| 7 |
TEMP_DIR = os.path.abspath("temp/")
|
utils/process_video.py
CHANGED
|
@@ -19,14 +19,14 @@ def process_video(invideo_filename:str,
|
|
| 19 |
font:str,
|
| 20 |
bg_color:str,
|
| 21 |
text_color:str,
|
| 22 |
-
|
| 23 |
):
|
| 24 |
invideo_filename = os.path.normpath(invideo_filename)
|
| 25 |
invideo_path_parts = invideo_filename.split(os.path.sep)
|
| 26 |
VIDEO_NAME = invideo_path_parts[-1]
|
| 27 |
OUTVIDEO_PATH = os.path.join(invideo_path_parts[-3], invideo_path_parts[-2], f"result_{VIDEO_NAME}")
|
| 28 |
if srt_path:
|
| 29 |
-
subtitler(invideo_filename, srt_path, OUTVIDEO_PATH, fontsize, font, bg_color, text_color,
|
| 30 |
return OUTVIDEO_PATH, srt_path
|
| 31 |
logging.info("Converting Video to Audio")
|
| 32 |
INAUDIO_PATH = os.path.abspath(f"{invideo_filename.split('.')[0]}.m4a")
|
|
@@ -37,5 +37,5 @@ def process_video(invideo_filename:str,
|
|
| 37 |
if not os.path.exists(SRT_PATH):
|
| 38 |
transcriber(INAUDIO_PATH, SRT_PATH, max_words_per_line, task)
|
| 39 |
logging.info("Subtitling...")
|
| 40 |
-
subtitler(invideo_filename, SRT_PATH, OUTVIDEO_PATH, fontsize, font, bg_color, text_color,
|
| 41 |
return OUTVIDEO_PATH, SRT_PATH
|
|
|
|
| 19 |
font:str,
|
| 20 |
bg_color:str,
|
| 21 |
text_color:str,
|
| 22 |
+
caption_mode:str
|
| 23 |
):
|
| 24 |
invideo_filename = os.path.normpath(invideo_filename)
|
| 25 |
invideo_path_parts = invideo_filename.split(os.path.sep)
|
| 26 |
VIDEO_NAME = invideo_path_parts[-1]
|
| 27 |
OUTVIDEO_PATH = os.path.join(invideo_path_parts[-3], invideo_path_parts[-2], f"result_{VIDEO_NAME}")
|
| 28 |
if srt_path:
|
| 29 |
+
subtitler(invideo_filename, srt_path, OUTVIDEO_PATH, fontsize, font, bg_color, text_color, caption_mode)
|
| 30 |
return OUTVIDEO_PATH, srt_path
|
| 31 |
logging.info("Converting Video to Audio")
|
| 32 |
INAUDIO_PATH = os.path.abspath(f"{invideo_filename.split('.')[0]}.m4a")
|
|
|
|
| 37 |
if not os.path.exists(SRT_PATH):
|
| 38 |
transcriber(INAUDIO_PATH, SRT_PATH, max_words_per_line, task)
|
| 39 |
logging.info("Subtitling...")
|
| 40 |
+
subtitler(invideo_filename, SRT_PATH, OUTVIDEO_PATH, fontsize, font, bg_color, text_color, caption_mode)
|
| 41 |
return OUTVIDEO_PATH, SRT_PATH
|
utils/subtitler.py
CHANGED
|
@@ -19,12 +19,14 @@ def parse_srt(srt_file):
|
|
| 19 |
i += 1
|
| 20 |
return subtitles
|
| 21 |
|
| 22 |
-
def filter_caption_width(
|
| 23 |
-
if
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
|
| 29 |
def subtitler(video_file:str,
|
| 30 |
srt_path:str,
|
|
@@ -33,7 +35,7 @@ def subtitler(video_file:str,
|
|
| 33 |
font: str,
|
| 34 |
bg_color:str,
|
| 35 |
text_color:str,
|
| 36 |
-
|
| 37 |
):
|
| 38 |
"""Add subtitles from an SRT file to a video."""
|
| 39 |
video_file = os.path.abspath(video_file)
|
|
@@ -42,14 +44,15 @@ def subtitler(video_file:str,
|
|
| 42 |
clip = VideoFileClip(filename=video_file, target_resolution=None)
|
| 43 |
subtitles = parse_srt(srt_path)
|
| 44 |
subtitle_clips = []
|
|
|
|
| 45 |
for start, end, text in subtitles:
|
| 46 |
# Create TextClip with specified styling
|
| 47 |
# To get a list of possible color and font values run: print(TextClip.list("font"), '\n\n', TextClip.list("color"))
|
| 48 |
txt_clip = TextClip(text, fontsize=fontsize, color=text_color, font=font, method='caption',
|
| 49 |
-
bg_color=bg_color, align='center', size=(clip.w*
|
| 50 |
txt_clip = txt_clip.set_position(('center', 'bottom')).set_duration(clip.duration).set_start(start).set_end(end)
|
| 51 |
subtitle_x_position = 'center'
|
| 52 |
-
subtitle_y_position = clip.h *
|
| 53 |
text_position = (subtitle_x_position, subtitle_y_position)
|
| 54 |
subtitle_clips.append(txt_clip.set_position(text_position))
|
| 55 |
video = CompositeVideoClip(size=None, clips=[clip] + subtitle_clips)
|
|
|
|
| 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,
|
|
|
|
| 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)
|
|
|
|
| 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)
|