Skip to content
81 changes: 74 additions & 7 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,77 @@ subtitle:
max_length: 75
# *Translated subtitles are slightly larger than source subtitles, affecting the reference length for subtitle splitting
target_multiplier: 1.2

# *Summary length, set low to 2k if using local LLM
# *Subtitle format for burning into video: 'srt' or 'ass'
format: 'srt'
# *SRT style settings (only effective when format is 'srt')
# *These map to ffmpeg force_style parameters
srt_style:
source:
fontname: 'Arial'
fontsize: 15
primary_color: '&HFFFFFF'
outline_color: '&H000000'
outline_width: 1.0
shadow_color: '&H80000000'
border_style: 1
alignment: 8
margin_v: 10
margin_l: 10
margin_r: 10
translation:
fontname: 'Arial'
fontsize: 17
primary_color: '&H00FFFF'
outline_color: '&H000000'
outline_width: 1.0
back_color: '&H33000000'
border_style: 4
alignment: 2
margin_v: 27
margin_l: 10
margin_r: 10
# *ASS style settings (only effective when format is 'ass')
# *scale_mode: 'absolute' = PlayResX/Y matches video resolution, fontsize = absolute pixels
# * 'relative' = PlayResX/Y from config below, fontsize scaled proportionally
ass_style:
scale_mode: 'absolute'
# --- 仅 relative 模式生效 ---
play_res_x: 1920
play_res_y: 1080
# ---------------------------
source:
fontname: 'Arial'
fontsize: 17
primary_color: '&HFFFFFF'
secondary_color: '&HFFFFFF'
outline_color: '&H000000'
back_color: '&H000000'
bold: 0
italic: 0
border_style: 1
outline_width: 1.0
shadow: 0.0
shadow_color: '&H80000000'
alignment: 8
margin_l: 10
margin_r: 10
margin_v: 10
translation:
fontname: 'Arial'
fontsize: 17
primary_color: '&H00FFFF'
secondary_color: '&H00FFFF'
outline_color: '&H000000'
back_color: '&H33000000'
bold: 0
italic: 0
border_style: 4
outline_width: 1.0
shadow: 0.0
alignment: 2
margin_l: 10
margin_r: 10
margin_v: 27
summary_length: 8000

# *Maximum number of words for the first rough cut, below 18 will cut too finely affecting translation, above 22 is too long and will make subsequent subtitle splitting difficult to align
Expand Down Expand Up @@ -133,8 +202,7 @@ tolerance: 1.5 # Allowed extension time to the next subtitle




## ======================== Additional settings ======================== ##
## ======================== Additional settings ========================##

# Whisper model directory
model_dir: './_model_cache'
Expand Down Expand Up @@ -163,8 +231,7 @@ spacy_model_map:
ja: 'ja_core_news_md'
es: 'es_core_news_md'
de: 'de_core_news_md'
it: 'it_core_news_md'
zh: 'zh_core_web_md'
it: 'it_core_web_md'

# Languages that use space as separator
language_split_with_space:
Expand All @@ -178,4 +245,4 @@ language_split_with_space:
# Languages that do not use space as separator
language_split_without_space:
- 'zh'
- 'ja'
- 'ja'
82 changes: 64 additions & 18 deletions core/_12_dub_to_vid.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import platform
import subprocess
import os

import cv2
import numpy as np
Expand All @@ -14,19 +15,61 @@

DUB_VIDEO = "output/output_dub.mp4"
DUB_SUB_FILE = 'output/dub.srt'
DUB_ASS_FILE = 'output/dub.ass'
DUB_AUDIO = 'output/dub.mp3'

TRANS_FONT_SIZE = 17
TRANS_FONT_NAME = 'Arial'
if platform.system() == 'Linux':
TRANS_FONT_NAME = 'NotoSansCJK-Regular'
if platform.system() == 'Darwin':
TRANS_FONT_NAME = 'Arial Unicode MS'
SRT_TRANS_DEFAULTS = {
'fontname': 'Arial', 'fontsize': 17,
'primary_color': '&H00FFFF', 'outline_color': '&H000000',
'outline_width': 1.0, 'back_color': '&H33000000', 'border_style': 4,
'alignment': 2, 'margin_v': 27, 'margin_l': 10, 'margin_r': 10,
}

TRANS_FONT_COLOR = '&H00FFFF'
TRANS_OUTLINE_COLOR = '&H000000'
TRANS_OUTLINE_WIDTH = 1
TRANS_BACK_COLOR = '&H33000000'
def _platform_fontname():
if platform.system() == 'Linux':
return 'NotoSansCJK-Regular'
elif platform.system() == 'Darwin':
return 'Arial Unicode MS'
return 'Arial'

def _build_dub_force_style():
defaults = SRT_TRANS_DEFAULTS
style = load_key("subtitle.srt_style.translation") or {}
merged = {**defaults, **style}
if 'fontname' not in style:
merged['fontname'] = _platform_fontname()
parts = [
f"FontSize={merged['fontsize']}",
f"FontName={merged['fontname']}",
f"PrimaryColour={merged['primary_color']}",
f"OutlineColour={merged['outline_color']}",
f"OutlineWidth={merged['outline_width']}",
f"BackColour={merged['back_color']}",
f"Alignment={merged['alignment']}",
f"MarginV={merged['margin_v']}",
f"BorderStyle={merged['border_style']}",
]
return ','.join(parts)

def _generate_dub_ass():
from core.utils.ass_utils import generate_ass
import pandas as pd

style_config = load_key("subtitle.ass_style") or {}
df, lines, new_sub_times = None, None, None

from core._11_merge_audio import load_and_flatten_data
df, lines, new_sub_times = load_and_flatten_data(_8_1_AUDIO_TASK)

rows = []
for i, ((start_time, end_time), line) in enumerate(zip(new_sub_times, lines)):
rows.append({
'timestamp': f"{int(start_time//3600):02d}:{int((start_time%3600)//60):02d}:{int(start_time%60):02d},{int((start_time*1000)%1000):03d} --> {int(end_time//3600):02d}:{int((end_time%3600)//60):02d}:{int(end_time%60):02d},{int((end_time*1000)%1000):03d}",
'Translation': line,
})
df_ass = pd.DataFrame(rows)

generate_ass(df_ass, ['Translation'], DUB_ASS_FILE, style_config)

def merge_video_audio():
"""Merge video and audio, and reduce video volume"""
Expand Down Expand Up @@ -56,13 +99,16 @@ def merge_video_audio():
TARGET_HEIGHT = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
video.release()
rprint(f"[bold green]Video resolution: {TARGET_WIDTH}x{TARGET_HEIGHT}[/bold green]")

subtitle_filter = (
f"subtitles={DUB_SUB_FILE}:force_style='FontSize={TRANS_FONT_SIZE},"
f"FontName={TRANS_FONT_NAME},PrimaryColour={TRANS_FONT_COLOR},"
f"OutlineColour={TRANS_OUTLINE_COLOR},OutlineWidth={TRANS_OUTLINE_WIDTH},"
f"BackColour={TRANS_BACK_COLOR},Alignment=2,MarginV=27,BorderStyle=4'"
)

subtitle_format = load_key("subtitle.format") or 'srt'

if subtitle_format == 'ass':
if not os.path.exists(DUB_ASS_FILE):
_generate_dub_ass()
subtitle_filter = f"subtitles={DUB_ASS_FILE}"
else:
force_style = _build_dub_force_style()
subtitle_filter = f"subtitles={DUB_SUB_FILE}:force_style='{force_style}'"

cmd = [
'ffmpeg', '-y', '-i', VIDEO_FILE, '-i', background_file, '-i', normalized_dub_audio,
Expand All @@ -85,4 +131,4 @@ def merge_video_audio():
rprint(f"[bold green]Video and audio successfully merged into {DUB_VIDEO}[/bold green]")

if __name__ == '__main__':
merge_video_audio()
merge_video_audio()
17 changes: 14 additions & 3 deletions core/_6_gen_sub.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import autocorrect_py as autocorrect
from core.utils import *
from core.utils.models import *
from core.utils.ass_utils import generate_ass, ASS_OUTPUT_CONFIGS
console = Console()

SUBTITLE_OUTPUT_CONFIGS = [
Expand Down Expand Up @@ -153,15 +154,25 @@ def align_timestamp_main():
df_translate = pd.read_excel(_5_SPLIT_SUB)
df_translate['Translation'] = df_translate['Translation'].apply(clean_translation)

align_timestamp(df_text, df_translate, SUBTITLE_OUTPUT_CONFIGS, _OUTPUT_DIR)
df_trans_time = align_timestamp(df_text, df_translate, SUBTITLE_OUTPUT_CONFIGS, _OUTPUT_DIR)
console.print(Panel("[bold green]🎉📝 Subtitles generation completed! Please check in the `output` folder 👀[/bold green]"))

# for audio
df_translate_for_audio = pd.read_excel(_5_REMERGED) # use remerged file to avoid unmatched lines when dubbing
df_translate_for_audio = pd.read_excel(_5_REMERGED)
df_translate_for_audio['Translation'] = df_translate_for_audio['Translation'].apply(clean_translation)

align_timestamp(df_text, df_translate_for_audio, AUDIO_SUBTITLE_OUTPUT_CONFIGS, _AUDIO_DIR)
df_trans_time_audio = align_timestamp(df_text, df_translate_for_audio, AUDIO_SUBTITLE_OUTPUT_CONFIGS, _AUDIO_DIR)
console.print(Panel(f"[bold green]🎉📝 Audio subtitles generation completed! Please check in the `{_AUDIO_DIR}` folder 👀[/bold green]"))

subtitle_format = load_key("subtitle.format") or 'srt'
if subtitle_format == 'ass':
style_config = load_key("subtitle.ass_style") or {}

for filename, columns in ASS_OUTPUT_CONFIGS:
filepath = os.path.join(_OUTPUT_DIR, filename)
generate_ass(df_trans_time, columns, filepath, style_config)

console.print(Panel("[bold green]🎉📝 ASS subtitles generated! Please check in the `output` folder 👀[/bold green]"))


if __name__ == '__main__':
Expand Down
106 changes: 70 additions & 36 deletions core/_7_sub_into_vid.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,61 @@
import os, subprocess, time
import os, subprocess, time, platform
from core._1_ytdlp import find_video_files
import cv2
import numpy as np
import platform
from core.utils import *

SRC_FONT_SIZE = 15
TRANS_FONT_SIZE = 17
FONT_NAME = 'Arial'
TRANS_FONT_NAME = 'Arial'

# Linux need to install google noto fonts: apt-get install fonts-noto
if platform.system() == 'Linux':
FONT_NAME = 'NotoSansCJK-Regular'
TRANS_FONT_NAME = 'NotoSansCJK-Regular'
# Mac OS has different font names
elif platform.system() == 'Darwin':
FONT_NAME = 'Arial Unicode MS'
TRANS_FONT_NAME = 'Arial Unicode MS'

SRC_FONT_COLOR = '&HFFFFFF'
SRC_OUTLINE_COLOR = '&H000000'
SRC_OUTLINE_WIDTH = 1
SRC_SHADOW_COLOR = '&H80000000'
TRANS_FONT_COLOR = '&H00FFFF'
TRANS_OUTLINE_COLOR = '&H000000'
TRANS_OUTLINE_WIDTH = 1
TRANS_BACK_COLOR = '&H33000000'
from core.utils.models import *

OUTPUT_DIR = "output"
OUTPUT_VIDEO = f"{OUTPUT_DIR}/output_sub.mp4"
SRC_SRT = f"{OUTPUT_DIR}/src.srt"
TRANS_SRT = f"{OUTPUT_DIR}/trans.srt"

SRC_ASS = f"{OUTPUT_DIR}/src.ass"
TRANS_ASS = f"{OUTPUT_DIR}/trans.ass"

SRT_SRC_DEFAULTS = {
'fontname': 'Arial', 'fontsize': 15,
'primary_color': '&HFFFFFF', 'outline_color': '&H000000',
'outline_width': 1.0, 'shadow_color': '&H80000000', 'border_style': 1,
'alignment': 8, 'margin_v': 10, 'margin_l': 10, 'margin_r': 10,
}
SRT_TRANS_DEFAULTS = {
'fontname': 'Arial', 'fontsize': 17,
'primary_color': '&H00FFFF', 'outline_color': '&H000000',
'outline_width': 1.0, 'back_color': '&H33000000', 'border_style': 4,
'alignment': 2, 'margin_v': 27, 'margin_l': 10, 'margin_r': 10,
}

def _platform_fontname():
if platform.system() == 'Linux':
return 'NotoSansCJK-Regular'
elif platform.system() == 'Darwin':
return 'Arial Unicode MS'
return 'Arial'

def _build_srt_force_style(style_key):
defaults = SRT_SRC_DEFAULTS if style_key == 'source' else SRT_TRANS_DEFAULTS
config_key = f"subtitle.srt_style.{style_key}"
style = load_key(config_key) or {}
merged = {**defaults, **style}
if 'fontname' not in style:
merged['fontname'] = _platform_fontname()
parts = [
f"FontSize={merged['fontsize']}",
f"FontName={merged['fontname']}",
f"PrimaryColour={merged['primary_color']}",
f"OutlineColour={merged['outline_color']}",
f"OutlineWidth={merged['outline_width']}",
]
if style_key == 'source':
parts.append(f"ShadowColour={merged['shadow_color']}")
parts.append(f"BorderStyle={merged['border_style']}")
else:
parts.append(f"BackColour={merged['back_color']}")
parts.append(f"Alignment={merged['alignment']}")
parts.append(f"MarginV={merged['margin_v']}")
parts.append(f"BorderStyle={merged['border_style']}")
return ','.join(parts)

def check_gpu_available():
try:
result = subprocess.run(['ffmpeg', '-encoders'], capture_output=True, text=True)
Expand All @@ -44,7 +67,6 @@ def merge_subtitles_to_video():
video_file = find_video_files()
os.makedirs(os.path.dirname(OUTPUT_VIDEO), exist_ok=True)

# Check resolution
if not load_key("burn_subtitles"):
rprint("[bold yellow]Warning: A 0-second black video will be generated as a placeholder as subtitles are not burned in.[/bold yellow]")

Expand All @@ -58,26 +80,38 @@ def merge_subtitles_to_video():
rprint("[bold green]Placeholder video has been generated.[/bold green]")
return

if not os.path.exists(SRC_SRT) or not os.path.exists(TRANS_SRT):
rprint("Subtitle files not found in the 'output' directory.")
exit(1)
subtitle_format = load_key("subtitle.format") or 'srt'

video = cv2.VideoCapture(video_file)
TARGET_WIDTH = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
TARGET_HEIGHT = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
video.release()
rprint(f"[bold green]Video resolution: {TARGET_WIDTH}x{TARGET_HEIGHT}[/bold green]")

if subtitle_format == 'ass':
if not os.path.exists(SRC_ASS) or not os.path.exists(TRANS_ASS):
rprint("ASS subtitle files not found in the 'output' directory.")
exit(1)

src_style = f"subtitles={SRC_ASS}"
trans_style = f"subtitles={TRANS_ASS}"
else:
if not os.path.exists(SRC_SRT) or not os.path.exists(TRANS_SRT):
rprint("Subtitle files not found in the 'output' directory.")
exit(1)

src_force = _build_srt_force_style('source')
trans_force = _build_srt_force_style('translation')
src_style = f"subtitles={SRC_SRT}:force_style='{src_force}'"
trans_style = f"subtitles={TRANS_SRT}:force_style='{trans_force}'"

ffmpeg_cmd = [
'ffmpeg', '-i', video_file,
'-vf', (
f"scale={TARGET_WIDTH}:{TARGET_HEIGHT}:force_original_aspect_ratio=decrease,"
f"pad={TARGET_WIDTH}:{TARGET_HEIGHT}:(ow-iw)/2:(oh-ih)/2,"
f"subtitles={SRC_SRT}:force_style='FontSize={SRC_FONT_SIZE},FontName={FONT_NAME},"
f"PrimaryColour={SRC_FONT_COLOR},OutlineColour={SRC_OUTLINE_COLOR},OutlineWidth={SRC_OUTLINE_WIDTH},"
f"ShadowColour={SRC_SHADOW_COLOR},BorderStyle=1',"
f"subtitles={TRANS_SRT}:force_style='FontSize={TRANS_FONT_SIZE},FontName={TRANS_FONT_NAME},"
f"PrimaryColour={TRANS_FONT_COLOR},OutlineColour={TRANS_OUTLINE_COLOR},OutlineWidth={TRANS_OUTLINE_WIDTH},"
f"BackColour={TRANS_BACK_COLOR},Alignment=2,MarginV=27,BorderStyle=4'"
f"{src_style},"
f"{trans_style}"
).encode('utf-8'),
]

Expand Down
Loading