diff --git a/app/common/config.py b/app/common/config.py index c3965f5..ce3de55 100644 --- a/app/common/config.py +++ b/app/common/config.py @@ -205,6 +205,11 @@ class Config(QConfig): 0, RangeValidator(-5000, 5000) ) + + vertical_offset = RangeConfigItem( + "Subtitle", "VerticalOffset", + 0, RangeValidator(-500, 500) + ) # ------------------- 软件页面配置 ------------------- micaEnabled = ConfigItem("MainWindow", "MicaEnabled", False, BoolValidator()) diff --git a/app/common/enums.py b/app/common/enums.py index e60c50b..2834ca2 100644 --- a/app/common/enums.py +++ b/app/common/enums.py @@ -1,9 +1,13 @@ # Set the enums to new translated values from PyQt5.QtCore import QObject -from ..core.entities import SubtitleLayoutEnum, InternetTranslateEnum, TodoWhenDoneEnum, Task +from ..core.entities import SubtitleLayoutEnum, InternetTranslateEnum, TodoWhenDoneEnum, Task, BatchTaskTypeEnum def Enums_Translate(): qoEnums = QObject() + BatchTaskTypeEnum.TRANSCRIBE._value_ = qoEnums.tr("Create Subtitle from Audio/Video") + BatchTaskTypeEnum.SOFT._value_ = qoEnums.tr("Create Soft Subtitle Video") + BatchTaskTypeEnum.HARD._value_ = qoEnums.tr("Create Hard Subtitle Video") + SubtitleLayoutEnum.ONLY_ORIGINAL._value_ = qoEnums.tr("Original Only") SubtitleLayoutEnum.ONLY_TRANSLATE._value_ = qoEnums.tr("Translated Only") SubtitleLayoutEnum.ORIGINAL_ON_TOP._value_ = qoEnums.tr("Original on Top") @@ -34,8 +38,8 @@ def Enums_Translate(): Task.Source.FILE_IMPORT._value_ = qoEnums.tr("File Import") Task.Source.URL_IMPORT._value_ = qoEnums.tr("URL Import") - Task.Type.OPTIMIZE._value_ = qoEnums.tr("Optimize") - Task.Type.SUBTITLE._value_ = qoEnums.tr("Subtitle") - Task.Type.SYNTHESIS._value_ = qoEnums.tr("Synthesis") - Task.Type.TRANSCRIBE._value_ = qoEnums.tr("Transcribe") - Task.Type.URL._value_ = qoEnums.tr("URL Import") \ No newline at end of file + Task.Type.OPTIMIZE._value_ = qoEnums.tr("Optimize + Translate Subtitles") + Task.Type.SUBTITLE._value_ = qoEnums.tr("Add Subtitle To Video") + Task.Type.SYNTHESIS._value_ = qoEnums.tr("Combine Subtitle with Video") + Task.Type.TRANSCRIBE._value_ = qoEnums.tr("Get Subtitle From Video/Audio") + Task.Type.URL._value_ = qoEnums.tr("Download Video from URL then Add Subtitle") \ No newline at end of file diff --git a/app/common/signal_bus.py b/app/common/signal_bus.py index 5182dec..c39cab4 100644 --- a/app/common/signal_bus.py +++ b/app/common/signal_bus.py @@ -14,6 +14,9 @@ class SignalBus(QObject): # 使用网络翻译 internet_translation_changed = pyqtSignal(bool) internet_translation_method_changed = pyqtSignal(bool) + # + need_video_changed = pyqtSignal(bool) + soft_subtitle_changed = pyqtSignal(bool) # 新增视频控制相关信号 video_play = pyqtSignal() # 播放信号 @@ -23,6 +26,12 @@ class SignalBus(QObject): video_segment_play = pyqtSignal(int, int) # 播放片段信号,参数为开始和结束时间(ms) video_subtitle_added = pyqtSignal(str) # 添加字幕文件信号 + def on_need_video_changed(self, needVideo: bool): + self.need_video_changed.emit(needVideo) + + def on_soft_subtitle_changed(self, softSubtitle: bool): + self.soft_subtitle_changed.emit(softSubtitle) + def on_subtitle_layout_changed(self, layout: str): self.subtitle_layout_changed.emit(layout) diff --git a/app/core/entities.py b/app/core/entities.py index f3fd1da..312e3bf 100644 --- a/app/core/entities.py +++ b/app/core/entities.py @@ -6,6 +6,12 @@ from typing import Optional from PyQt5.QtCore import QObject +class BatchTaskTypeEnum(Enum): + """ 批量任务类型 """ + TRANSCRIBE = "Create Subtitle from Audio/Video" + SOFT = "Create Soft Subtitle Video" + HARD = "Create Hard Subtitle Video" + class SubtitleLayoutEnum(Enum): """ 字幕布局 """ ONLY_ORIGINAL = "Original Only" @@ -19,6 +25,15 @@ class InternetTranslateEnum(Enum): GOOGLE = "Google Translate" +class SupportedImageFormats(Enum): + """ 支持的图片格式 """ + JPG = "jpg" + PNG = "png" + BMP = "bmp" + GIF = "gif" + WEBP = "webp" + + class SupportedAudioFormats(Enum): """ 支持的音频格式 """ AAC = "aac" @@ -492,11 +507,11 @@ class Source(Enum): class Type(Enum): # 任务类型:transcribe or generate subtitle - TRANSCRIBE = "Transcription" - SUBTITLE = "Subtitle" - OPTIMIZE = "Optimization" - SYNTHESIS = "Synthesis" - URL = "URL" + TRANSCRIBE = "Get Subtitle From Video/Audio" + SUBTITLE = "Add Subtitle To Video" + OPTIMIZE = "Optimize + Translate Subtitles" + SYNTHESIS = "Combine Subtitle with Video" + URL = "Download Video from URL then Add Subtitle" # 任务信息 id: int = field(default_factory=lambda: randint(0, 100_000_000)) @@ -522,7 +537,6 @@ class Type(Enum): # 转录(转录模型) transcribe_model: Optional[TranscribeModelEnum] = TranscribeModelEnum.JIANYING - transcribe_language: Optional[TranscribeLanguageEnum] = LANGUAGES[TranscribeLanguageEnum.ENGLISH.value] use_asr_cache: bool = True need_word_time_stamp: bool = False @@ -559,10 +573,16 @@ class Type(Enum): subtitle_layout: Optional[str] = None max_word_count_cjk: int = 12 max_word_count_english: int = 18 - need_split: bool = True + need_split: bool = False # 视频生成 need_video: bool = True video_save_path: Optional[str] = None soft_subtitle: bool = True subtitle_style_srt: Optional[str] = None + portrait: bool = False + portrait_background: Optional[str] = None + zoom_video: int = 100 + zoom_subtitle: int = 100 + vertical_offset: int = 0 + diff --git a/app/core/subtitle_processor/optimizer.py b/app/core/subtitle_processor/optimizer.py index d84c569..791159e 100644 --- a/app/core/subtitle_processor/optimizer.py +++ b/app/core/subtitle_processor/optimizer.py @@ -274,6 +274,8 @@ def translate_single_batch(self, original_subtitle: Dict[int,str], callback = No return_text = response.choices[0].message.content # logger.info(f"response:{type(return_text)}") previous_translation = return_text + + logger.info(f"{key}. Original: {value}\n{key}. Translated: {return_text}") line = {str(key): return_text} # Create a dictionary with key and translated text diff --git a/app/core/thread/create_task_thread.py b/app/core/thread/create_task_thread.py index a9bdf79..4d38c20 100644 --- a/app/core/thread/create_task_thread.py +++ b/app/core/thread/create_task_thread.py @@ -21,23 +21,20 @@ class CreateTaskThread(QThread): progress = pyqtSignal(int, str) error = pyqtSignal(str) - def __init__(self, file_path, task_type: Task.Type): + def __init__(self, file_path, task_type: Task.Type, soft_sub: bool): super().__init__() self.file_path = file_path self.task_type = task_type + self.soft_sub = soft_sub def run(self): try: if self.task_type == Task.Type.SUBTITLE: - self.create_file_task(self.file_path) + self.create_file_task(self.file_path, self.soft_sub) elif self.task_type == Task.Type.URL: - self.create_url_task(self.file_path) + self.create_url_task(self.file_path, self.soft_sub) elif self.task_type == Task.Type.TRANSCRIBE: self.create_transcription_task(self.file_path) - elif self.task_type == Task.Type.OPTIMIZE: - self.create_subtitle_optimization_task() - elif self.task_type == Task.Type.SYNTHESIS: - self.create_video_synthesis_task() else: raise ValueError("No matching task type.") except Exception as e: @@ -45,7 +42,7 @@ def run(self): self.progress.emit(0, self.tr("创建任务失败")) self.error.emit(str(e)) - def create_file_task(self, file_path): + def create_file_task(self, file_path, soft_sub: bool): logger.info("\n===================") logger.info(f"开始创建文件任务:{file_path}") # 使用 Path 对象处理路径 @@ -91,7 +88,8 @@ def create_file_task(self, file_path): if cfg.subtitle_output_format.value.value == "ass": ass_style_name = cfg.subtitle_style_name.value ass_style_path = SUBTITLE_STYLE_PATH / f"{ass_style_name}.txt" - subtitle_style_srt = ass_style_path.read_text(encoding="utf-8") + if ass_style_path.exists(): + subtitle_style_srt = ass_style_path.read_text(encoding="utf-8") else: subtitle_style_srt = None @@ -149,16 +147,17 @@ def create_file_task(self, file_path): result_subtitle_save_path=str(result_subtitle_save_path), subtitle_layout=cfg.subtitle_layout.value, video_save_path=str(video_save_path), - soft_subtitle=cfg.soft_subtitle.value, + soft_subtitle=soft_sub, subtitle_style_srt=subtitle_style_srt, need_video=cfg.need_video.value, + vertical_offset=cfg.vertical_offset.value, type=Task.Type.SUBTITLE, ) self.finished.emit(task) self.progress.emit(100, self.tr("创建任务完成")) logger.info(f"文件任务创建完成:{task}") - def create_url_task(self, url, task_type): + def create_url_task(self, url, soft_sub: bool): logger.info("\n===================") logger.info(f"开始创建URL任务:{url}") self.progress.emit(5, self.tr("正在获取视频信息")) @@ -213,7 +212,8 @@ def create_url_task(self, url, task_type): if cfg.subtitle_output_format.value.value == "ass" and ass_style_path.exists(): ass_style_name = cfg.subtitle_style_name.value ass_style_path = SUBTITLE_STYLE_PATH / f"{ass_style_name}.txt" - subtitle_style_srt = ass_style_path.read_text(encoding="utf-8") + if ass_style_path.exists(): + subtitle_style_srt = ass_style_path.read_text(encoding="utf-8") else: subtitle_style_srt = None @@ -266,9 +266,10 @@ def create_url_task(self, url, task_type): result_subtitle_save_path=str(result_subtitle_save_path), subtitle_layout=cfg.subtitle_layout.value, video_save_path=str(video_save_path), - soft_subtitle=cfg.soft_subtitle.value, + soft_subtitle=soft_sub, subtitle_style_srt=subtitle_style_srt, need_video=cfg.need_video.value, + vertical_offset=cfg.vertical_offset.value, task=Task.Type.SUBTITLE, ) self.finished.emit(task) @@ -307,10 +308,11 @@ def create_transcription_task(self, file_path): original_subtitle_save_path = task_work_dir / f"【原始字幕】{file_name}-{cfg.transcribe_model.value.value}-{whisper_type}.srt" result_subtitle_save_path = file_full_path.parent / ( cfg.subtitle_file_prefix.value + file_name + cfg.subtitle_file_suffix.value + "." + cfg.subtitle_output_format.value.value ) - if cfg.subtitle_output_format.value.value == "ass" and ass_style_path.exists(): + if cfg.subtitle_output_format.value.value == "ass": ass_style_name = cfg.subtitle_style_name.value ass_style_path = SUBTITLE_STYLE_PATH / f"{ass_style_name}.txt" - subtitle_style_srt = ass_style_path.read_text(encoding="utf-8") + if ass_style_path.exists(): + subtitle_style_srt = ass_style_path.read_text(encoding="utf-8") else: subtitle_style_srt = None @@ -414,7 +416,7 @@ def create_subtitle_optimization_task(file_path): logger.info(f"字幕优化任务创建完成:{task}") return task - def create_video_synthesis_task(subtitle_file, video_file): + def create_video_synthesis_task(subtitle_file, video_file, soft_sub: bool): logger.info(f"开始创建视频合成任务:{subtitle_file} {video_file}") subtitle_file = Path(subtitle_file.strip()).as_posix() video_file = Path(video_file.strip()).as_posix() @@ -430,7 +432,7 @@ def create_video_synthesis_task(subtitle_file, video_file): status=Task.Status.GENERATING, work_dir=str(task_work_dir), file_path=str(Path(video_file)), - result_subtitle_save_path=str(Path(subtitle_file)), + original_subtitle_save_path=str(Path(subtitle_file)), video_save_path=str(video_save_path), soft_subtitle=cfg.soft_subtitle.value, type=Task.Type.SYNTHESIS, @@ -500,7 +502,6 @@ def sanitize_filename(name, replacement="_"): # 如果文件名为空,返回一个默认名称 if not sanitized: sanitized = "default_filename" - return sanitized diff --git a/app/core/thread/subtitle_optimization_thread.py b/app/core/thread/subtitle_optimization_thread.py index ec8d1ac..9c52261 100644 --- a/app/core/thread/subtitle_optimization_thread.py +++ b/app/core/thread/subtitle_optimization_thread.py @@ -129,9 +129,10 @@ def run(self): asr_data = from_subtitle_file(str_path) # 检查是否需要合并重新断句 - if not asr_data.is_word_timestamp() and need_split and self.task.faster_whisper_one_word: + is_word_split = asr_data.is_word_timestamp() + if not is_word_split and need_split and self.task.faster_whisper_one_word: asr_data.split_to_word_segments() - if asr_data.is_word_timestamp(): + if is_word_split: self.progress.emit(15, self.tr("字幕断句...")) logger.info("正在字幕断句...") asr_data = merge_segments(asr_data, model=llm_model, @@ -141,6 +142,8 @@ def run(self): asr_data.save(save_path=split_path) self.update_all.emit(asr_data.to_json()) + + # 制作成请求llm接口的格式 {{"1": "original_subtitle"},...} subtitle_json = {str(k): v["original_subtitle"] for k, v in asr_data.to_json().items()} self.subtitle_length = len(subtitle_json) diff --git a/app/core/thread/subtitle_pipeline_thread.py b/app/core/thread/subtitle_pipeline_thread.py index 25c5db3..b97dc99 100644 --- a/app/core/thread/subtitle_pipeline_thread.py +++ b/app/core/thread/subtitle_pipeline_thread.py @@ -65,15 +65,16 @@ def handle_error(error_msg): # 3. 视频合成 # self.task.status = Task.Status.GENERATING - self.progress.emit(80, self.tr("开始合成视频")) - synthesis_thread = VideoSynthesisThread(self.task) - synthesis_thread.progress.connect(lambda value, msg: self.progress.emit(int(70 + value * 0.3), msg)) - synthesis_thread.error.connect(handle_error) - synthesis_thread.run() + if self.task.need_video: + self.progress.emit(80, self.tr("开始合成视频")) + synthesis_thread = VideoSynthesisThread(self.task) + synthesis_thread.progress.connect(lambda value, msg: self.progress.emit(int(70 + value * 0.3), msg)) + synthesis_thread.error.connect(handle_error) + synthesis_thread.run() - if self.has_error: - logger.info("视频合成过程中发生错误,终止流程") - return + if self.has_error: + logger.info("视频合成过程中发生错误,终止流程") + return self.task.status = Task.Status.COMPLETED logger.info("处理完成") diff --git a/app/core/thread/video_synthesis_thread.py b/app/core/thread/video_synthesis_thread.py index 23991d8..bc97983 100644 --- a/app/core/thread/video_synthesis_thread.py +++ b/app/core/thread/video_synthesis_thread.py @@ -2,8 +2,8 @@ from pathlib import Path from PyQt5.QtCore import QThread, pyqtSignal -from ..entities import Task -from ..utils.video_utils import add_subtitles +from ..entities import Task, VideoInfo +from ..utils.video_utils import add_subtitles, get_video_info from ..utils.logger import setup_logger from ...common.config import cfg @@ -37,12 +37,12 @@ def run(self): logger.info(f"时间:{datetime.datetime.now()}") self.task.status = Task.Status.SYNTHESIZING video_file = self.task.file_path - if Path(self.task.original_subtitle_save_path).is_file(): + if self.task.original_subtitle_save_path and Path(self.task.original_subtitle_save_path).is_file(): # result sub exist (after optimizing) - subtitle_file = self.task.result_subtitle_save_path - elif Path(self.task.result_subtitle_save_path).is_file(): - # No optimzing, original sub only subtitle_file = self.task.original_subtitle_save_path + elif self.task.result_subtitle_save_path and Path(self.task.result_subtitle_save_path).is_file(): + # No optimzing, original sub only + subtitle_file = self.task.result_subtitle_save_path else: raise RuntimeError("No subtitle file available.") @@ -61,7 +61,30 @@ def run(self): logger.info(f"开始合成视频: {video_file}") self.progress.emit(10, self.tr("正在合成")) self.progress.emit(11, f"Soft subtitle:{soft_subtitle}") + if not self.task.video_info: + video_info = get_video_info(video_file) + w = video_info["width"] + h = video_info["height"] + duration = int(video_info["duration_seconds"]) + else: + w = self.task.video_info.width + h = self.task.video_info.height + duration = int(self.task.video_info.duration_seconds) + + if self.task.portrait: + width = h + height = w + else: + width = w + height = h + add_subtitles(video_file, subtitle_file, video_save_path, soft_subtitle=soft_subtitle, + output_width=width, output_height=height, portrait=self.task.portrait, + vertical_offset=self.task.vertical_offset, + portrait_background=self.task.portrait_background, + duration=duration, + zoom_video=self.task.zoom_video, + zoom_subtitle=self.task.zoom_subtitle, progress_callback=self.progress_callback) self.progress.emit(100, self.tr("合成完成")) logger.info(f"视频合成完成,保存路径: {video_save_path}") diff --git a/app/core/utils/video_utils.py b/app/core/utils/video_utils.py index 5331660..6898b8a 100644 --- a/app/core/utils/video_utils.py +++ b/app/core/utils/video_utils.py @@ -103,10 +103,18 @@ def add_subtitles( input_file: str, subtitle_file: str, output: str, + duration: int, + output_width: int = None, + output_height: int = None, quality: Literal[ 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'] = 'medium', vcodec: str = 'libx264', soft_subtitle: bool = False, + portrait: bool = False, + portrait_background: str = None, + vertical_offset: int = 0, + zoom_video: int = 100, + zoom_subtitle: int = 100, progress_callback: callable = None ) -> None: assert Path(input_file).is_file(), qoVideo.tr("输入文件不存在") @@ -133,6 +141,7 @@ def add_subtitles( 'ffmpeg', '-i', input_file, '-i', subtitle_file, + '-map', '0', # 复制所有流 '-c:v', 'copy', '-c:a', 'copy', '-c:s', 'mov_text', @@ -163,7 +172,6 @@ def add_subtitles( else: logger.info("使用硬字幕") subtitle_file = Path(subtitle_file).as_posix().replace(':', r'\:') - vf = f"subtitles='{subtitle_file}'" if Path(output).suffix.lower() == '.webm': vcodec = 'libvpx-vp9' logger.info("WebM格式视频,使用libvpx-vp9编码器") @@ -174,31 +182,94 @@ def add_subtitles( if use_cuda: logger.info("使用CUDA加速") cmd.extend(['-hwaccel', 'cuda']) + if vcodec == 'libx264': + vcodec = 'h264_nvenc' + elif vcodec == 'libx265': + vcodec = 'hevc_nvenc' + cmd.extend([ '-i', input_file, + ]) + + print (f"portrait: {portrait}, background{portrait_background}\n" \ + +f"output_width: {output_width}, output_height: {output_height}, duration{duration}") + + if portrait and output_width and output_height: + # output_height = 1920, output_width = 1080, squeeze_height = 606 + squeeze_height = int( output_width * output_width / output_height /2) * 2 # The original video's height after rotating. + # Zoom of video and subtitles, all need to be multiple of 2 + output_width_subtitle = int(output_width * zoom_subtitle // 200) * 2 + output_height_subtitle = int(squeeze_height * zoom_subtitle // 200) * 2 + squeeze_height_subtitle = int( squeeze_height * zoom_subtitle // 200 ) * 2 + subtitle_x = (output_width - output_width_subtitle) //2 + subtitle_y = (output_height - squeeze_height_subtitle) // 2 + + output_width_video = int(output_width * zoom_video // 200) * 2 + output_height_video = int(squeeze_height * zoom_video // 200) * 2 + squeeze_height_video = int(squeeze_height * zoom_video // 200) * 2 + video_x = (output_width - output_width_video) // 2 + video_y = (output_height - output_height_video) // 2 + + + if portrait_background: + cmd.extend([ + '-i', portrait_background, + ]) + # With picture background + vf = f"[1:v]trim=0:{duration},scale={output_width}:{output_height}[bg];" \ + + f"color=d={duration}:c=black@0:s={output_width_subtitle}x{squeeze_height_subtitle}," \ + + f"subtitles='{subtitle_file}':alpha=1[sub];[0:v]scale={output_width_video}:{squeeze_height_video}[fg];" \ + + f"[bg][fg]overlay={video_x}:{video_y}[out];[out][sub]overlay={subtitle_x}:{subtitle_y+vertical_offset},setsar=1" + + else: # Blur background + vf = f"[0:v]avgblur=sizeX=40:sizeY=40,scale={output_width}x{output_height}:flags=fast_bilinear[bg];" \ + + f"color=d={duration}:c=black@0:s={output_width_subtitle}x{squeeze_height_subtitle}," \ + + f"subtitles='{subtitle_file}':alpha=1[sub];[0:v]scale={output_width_video}:{squeeze_height_video}[fg];" \ + + f"[bg][fg]overlay={video_x}:{video_y}[out];[out][sub]overlay={subtitle_x}:{subtitle_y+vertical_offset},setsar=1" + else: + # Just landscape subtitle + vf = f"color=d={duration}:c=black@0:s={output_width_subtitle}x{output_height_subtitle}," \ + + f"subtitles='{subtitle_file}':alpha=1[sub];[0:v][sub]overlay={subtitle_x}:{subtitle_y+vertical_offset},setsar=1" + + cmd.extend([ '-map', '0', # 复制所有流 + '-map', '-0:v', # 排除视频流,免得生成两个视频流 '-acodec', 'copy', '-vcodec', vcodec, - '-c:s', 'copy' + '-c:s', 'copy', '-preset', quality, - '-vf', vf, + '-filter_complex', q(vf), '-y', # 覆盖输出文件 output ]) - + cmd_str = subprocess.list2cmdline(cmd) + cmd_str = cmd_str.replace('\\"', '"') # Fix the quote problem of video filters logger.info(f"添加硬字幕执行命令: {cmd_str}") try: - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - encoding='utf-8', - errors='replace', - creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0, - ) + if os.name == 'nt': + # In windows system, the -filter_complex need quoting. + process = subprocess.Popen( + cmd_str, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding='utf-8', + errors='replace', + creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0, + ) + else: + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding='utf-8', + errors='replace', + creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0, + ) + # 实时读取输出并调用回调函数 total_duration = None diff --git a/app/view/batch_process_interface.py b/app/view/batch_process_interface.py index a10ebcb..c162cb7 100644 --- a/app/view/batch_process_interface.py +++ b/app/view/batch_process_interface.py @@ -11,15 +11,16 @@ from qfluentwidgets import ComboBox, CardWidget, ToolTipFilter, FluentWindow, isDarkTheme, \ ToolTipPosition, PrimaryPushButton, PushButton, InfoBar, BodyLabel, PillPushButton, setFont, \ InfoBadgePosition, ProgressRing, InfoBarPosition, ScrollArea, Action, RoundMenu, IconInfoBadge, \ - InfoLevel + InfoLevel, SwitchButton, IndicatorPosition from qfluentwidgets import FluentIcon as FIF from qframelesswindow import FramelessWindow, StandardTitleBar from ..config import RESOURCE_PATH from ..common.config import cfg +from ..common.signal_bus import signalBus from ..components.EnumComboBoxSettingCard import EnumComboBoxSettingCard, EnumOptionsValidator, EnumExSerializer -from ..core.entities import SupportedVideoFormats, SupportedAudioFormats, TodoWhenDoneEnum -from ..core.entities import Task, VideoInfo +from ..core.entities import SupportedVideoFormats, SupportedAudioFormats, TodoWhenDoneEnum, SupportedSubtitleFormats, SupportedImageFormats +from ..core.entities import Task, VideoInfo, BatchTaskTypeEnum from ..core.thread.create_task_thread import CreateTaskThread from ..core.thread.subtitle_pipeline_thread import SubtitlePipelineThread from ..core.thread.transcript_thread import TranscriptThread @@ -75,9 +76,8 @@ def setup_ui(self): # 任务类型选择 self.task_type_combo = ComboBox(self) - self.task_type_combo.addItems([self.tr("视频加字幕"), self.tr("音视频转录")]) - if not cfg.need_video.value: # Set it according to the configuration - self.task_type_combo.setCurrentIndex(1) + self.task_type_combo.addItems(job.value for job in BatchTaskTypeEnum) + self.set_default_task_type(True) self.top_layout.addWidget(self.task_type_combo) @@ -131,6 +131,21 @@ def setup_signals(self): self.start_all_button.clicked.connect(self.start_batch_process) self.cancel_button.clicked.connect(self.cancel_batch_process) self.todo_when_done_combobox.currentTextChanged.connect(self.todo_when_done_changed) + + signalBus.need_video_changed.connect(self.set_default_task_type) + signalBus.soft_subtitle_changed.connect(self.set_default_task_type) + + def set_default_task_type(self, whatever): + # Set it according to the configuration + if cfg.need_video.value: + if cfg.soft_subtitle.value: + # Create soft sub video + self.task_type_combo.setCurrentText(BatchTaskTypeEnum.SOFT.value) + else: + # Create hard sub video + self.task_type_combo.setCurrentText(BatchTaskTypeEnum.HARD.value) + else: + self.task_type_combo.setCurrentText(BatchTaskTypeEnum.TRANSCRIBE.value) def todo_when_done_changed(self, text: str): todoKey = None @@ -341,25 +356,32 @@ def on_add_file(self): # 构建文件过滤器字符串 video_formats = [f"*.{fmt.value}" for fmt in SupportedVideoFormats] audio_formats = [f"*.{fmt.value}" for fmt in SupportedAudioFormats] - if self.task_type_combo.currentText() == self.tr("视频加字幕"): + if self.task_type_combo.currentText() == BatchTaskTypeEnum.SOFT.value: # Create soft sub video + filter_str = f"{self.tr('视频文件')} ({' '.join(video_formats)})" + task_type = Task.Type.SUBTITLE + soft_sub = True + elif self.task_type_combo.currentText() == BatchTaskTypeEnum.HARD.value: # Create hard sub video filter_str = f"{self.tr('视频文件')} ({' '.join(video_formats)})" task_type = Task.Type.SUBTITLE + soft_sub = False else: # 音频/视频生成字幕 filter_str = f"{self.tr('音频文件或视频文件')} ({' '.join(audio_formats + video_formats)})" task_type = Task.Type.TRANSCRIBE + soft_sub = True files, _ = QFileDialog.getOpenFileNames(self, self.tr("选择文件"), cfg.last_open_dir.value , filter_str) for file_path in files: - self.create_task(file_path, task_type) + self.create_task(file_path, task_type, soft_sub) # Save the files' directory for later use - file_dir = str( Path(files[0]).parent ) - if file_dir != cfg.last_open_dir.value: - cfg.last_open_dir.value = file_dir - cfg.save() + if files: + file_dir = str( Path(files[0]).parent ) + if file_dir != cfg.last_open_dir.value: + cfg.last_open_dir.value = file_dir + cfg.save() - def create_task(self, file_path, task_type: Task.Type): + def create_task(self, file_path, task_type: Task.Type, soft_sub: bool): """创建新任务""" # 检查文件是否已存在 for task in self.tasks: @@ -373,8 +395,7 @@ def create_task(self, file_path, task_type: Task.Type): ) return - # task_type = 'transcription' if self.task_type_combo.currentText() == self.tr("音视频转录") else 'file' - create_thread = CreateTaskThread(file_path, task_type) + create_thread = CreateTaskThread(file_path, task_type, soft_sub) create_thread.finished.connect(self.add_task_card) create_thread.finished.connect(lambda: self.cleanup_thread(create_thread)) self.create_threads.append(create_thread) @@ -457,17 +478,22 @@ def dropEvent(self, event): file_ext = os.path.splitext(file_path)[1][1:].lower() # 根据任务类型检查文件格式 - if self.task_type_combo.currentText() == self.tr("视频加字幕"): + if self.task_type_combo.currentText() in [BatchTaskTypeEnum.SOFT.value, BatchTaskTypeEnum.HARD.value]: + # Create soft or hard sub video supported_formats = {fmt.value for fmt in SupportedVideoFormats} task_type = Task.Type.SUBTITLE else: + # Create subtitle only supported_formats = {fmt.value for fmt in SupportedVideoFormats} | {fmt.value for fmt in SupportedAudioFormats} task_type = Task.Type.TRANSCRIBE if file_ext in supported_formats: - self.create_task(file_path, task_type) + if self.task_type_combo.currentText() == BatchTaskTypeEnum.HARD.value: + self.create_task(file_path, task_type, False) # Hard coded subtitles + else: + self.create_task(file_path, task_type, True) # Soft coded subtitles else: - error_msg = self.tr("请拖入视频文件") if self.task_type_combo.currentText() == self.tr("视频加字幕") else self.tr("请拖入音频或视频文件") + error_msg = self.tr("请拖入视频文件") if self.task_type_combo.currentText() == BatchTaskTypeEnum.TRANSCRIBE.value else self.tr("请拖入音频或视频文件") InfoBar.error( self.tr(f"格式错误") + file_ext, error_msg, @@ -501,7 +527,7 @@ def __init__(self, parent=None): def setup_ui(self): self.setFixedHeight(180) self.layout = QHBoxLayout(self) - self.layout.setContentsMargins(15, 10, 15, 10) + self.layout.setContentsMargins(15, 5, 15, 5) self.layout.setSpacing(10) # 设置缩略图 @@ -528,22 +554,29 @@ def setup_info_layout(self): # 设置视频标题 self.video_title = BodyLabel(self.tr("未选择视频"), self) - self.video_title.setFont(QFont("Microsoft YaHei", 14)) + self.video_title.setFont(QFont("Microsoft YaHei", 12)) self.video_title.setWordWrap(True) self.info_layout.addWidget(self.video_title, alignment=Qt.AlignTop) # 设置视频详细信息 self.details_layout1 = QHBoxLayout() - self.details_layout1.setSpacing(15) + self.details_layout1.setSpacing(10) self.details_layout2 = QHBoxLayout() - self.details_layout2.setSpacing(15) + self.details_layout2.setSpacing(10) self.resolution_info = self.create_pill_button(self.tr("画质"), 120) self.file_size_info = self.create_pill_button(self.tr("文件大小"), 120) self.duration_info = self.create_pill_button(self.tr("时长"), 120) self.video_codec = self.create_pill_button(self.tr("视频码"), 120) self.audio_codec = self.create_pill_button(self.tr("音频码"), 120) - + + self.portrait_mode = SwitchButton(self, indicatorPos = IndicatorPosition.RIGHT) + self.portrait_mode.setOnText(self.tr("竖屏")) + self.portrait_mode.setOffText(self.tr("横屏")) + self.portrait_background = PushButton(self.tr("背景:无"), parent=self) + self.portrait_background.hide() + + self.progress_ring = ProgressRing(self) self.progress_ring.setFixedSize(20, 20) self.progress_ring.setStrokeWidth(4) @@ -556,6 +589,9 @@ def setup_info_layout(self): self.details_layout1.addStretch(1) self.details_layout2.addWidget(self.video_codec) self.details_layout2.addWidget(self.audio_codec) + self.details_layout2.addWidget(self.portrait_mode) + self.details_layout2.addWidget(self.portrait_background) + self.details_layout2.addStretch(1) self.info_layout.addLayout(self.details_layout1) self.info_layout.addLayout(self.details_layout2) @@ -590,7 +626,6 @@ def mouseDoubleClickEvent(self, event): def update_info(self, video_info: VideoInfo): """更新视频信息显示""" - # self.video_title.setText(video_info.file_name.rsplit('.', 1)[0]) self.video_title.setText(video_info.file_name + '\n' + video_info.file_path) self.resolution_info.setText(self.tr("画质: ") + f"{video_info.width}x{video_info.height}") file_size_mb = os.path.getsize(self.task.file_path) / 1024 / 1024 @@ -601,22 +636,41 @@ def update_info(self, video_info: VideoInfo): self.audio_codec.setText(self.tr("音频码 ") + video_info.audio_codec) # self.start_button.setDisabled(False) self.update_thumbnail(video_info.thumbnail_path) + if self.task and self.task.type == Task.Type.SUBTITLE and not cfg.soft_subtitle.value: + # When need to hard code subtitles, enable it. + self.portrait_mode.setDisabled(False) + else: + # Other cases, disable it. + self.portrait_mode.setDisabled(True) + self.update_tooltip() def update_tooltip(self): """更新tooltip""" # 设置整体tooltip - strategy_text = self.tr("无") - if self.task.need_optimize: - strategy_text = self.tr("字幕优化") - elif self.task.need_translate: - # strategy_text = self.tr("字幕优化+翻译 ") + str(self.task.target_language) - strategy_text = self.tr("字幕翻译 ") + str(self.task.target_language) - - tooltip = self.tr("转录模型: ") + self.task.transcribe_model.value + "\n" - tooltip += self.tr("文件: ") + self.task.file_path + '\n' - if self.task.status == Task.Status.PENDING: - tooltip += self.tr("字幕策略: ") + strategy_text + "\n" + + strategy_text = "" + if self.task.need_optimize or self.task.need_translate: + if self.task.need_optimize: + strategy_text += self.tr("翻译方式:智能多线程优化+翻译,目标:") + str(self.task.target_language) + " " + if self.task.need_translate: + strategy_text += self.tr("翻译方式:智能单线程单句翻译,目标:") + self.task.target_language + " " + strategy_text += self.tr(",使用的LLM 模型:") + self.task.llm_model + "" + if self.task.soft_subtitle: + strategy_text += self.tr("字幕类型:软字幕 ") + else: + strategy_text += self.tr("字幕类型:硬字幕 ") + if self.task.portrait: + strategy_text += self.tr("竖屏模式:开启 ") + if self.task.portrait_background: + strategy_text += "\n" + self.tr("竖屏背景:") + self.task.portrait_background + + tooltip = self.tr("任务类型:") + self.task.type.value + " " + self.tr("转录模型:") + self.task.transcribe_model.value + "\n" + if len(self.task.file_path) > 100: + tooltip += self.tr("文件: ") + self.task.file_path[:50] + "..." + Path(self.task.file_path).name + "\n" + else: + tooltip += self.tr("文件: ") + self.task.file_path + '\n' + tooltip += strategy_text + "\n" tooltip += self.tr("任务状态: ") + self.task.status.value self.setToolTip(tooltip) @@ -636,6 +690,37 @@ def setup_signals(self): self.start_button.clicked.connect(self.start) self.open_folder_button.clicked.connect(self.on_open_folder_clicked) self.preview_subtitle_button.clicked.connect(self.open_subtitle) + self.portrait_mode.checkedChanged.connect(self.on_portrait_mode_changed) + self.portrait_background.clicked.connect(self.on_portrait_background_clicked) + + def on_portrait_mode_changed(self, checked): + """竖屏模式切换""" + self.task.portrait = checked + if checked: + self.portrait_background.show() + else: + self.portrait_background.hide() + self.update_tooltip() + + def on_portrait_background_clicked(self): + picture_formats = [f"*.{fmt.value}" for fmt in SupportedImageFormats] + file, _ = QFileDialog.getOpenFileName(self, self.tr("选择背景图片"), + cfg.last_open_dir.value, + self.tr("Image Files (") + " ".join(picture_formats) + ")") + if not file: + return + file_path = Path(file) + if not file_path.exists(): + InfoBar.warning( + self.tr("文件不存在"), + self.tr("请重新选择"), + duration=3000, + ) + return + + self.portrait_background.setText(self.tr("背景:") + file_path.name) + self.task.portrait_background = file + self.update_tooltip() def show_context_menu(self, pos): """显示右键菜单""" @@ -656,21 +741,38 @@ def show_context_menu(self, pos): menu.addAction(delete_action) reprocess_action = Action(FIF.SYNC, self.tr("重新处理"), self) - reprocess_action.triggered.connect(self.start) + reprocess_action.triggered.connect(self.reprocess) menu.addAction(reprocess_action) - cancel_action = Action(FIF.CANCEL, self.tr("取消任务"), self) + cancel_action = Action(FIF.CLOSE, self.tr("停止任务"), self) cancel_action.triggered.connect(self.cancel) menu.addAction(cancel_action) # 显示菜单 menu.exec_(self.mapToGlobal(pos)) + def reprocess(self): + self.status = Task.Status.PENDING + self.start() + def open_subtitle(self): """打开字幕优化界面""" preview_subtitle_path = Path(self.task.original_subtitle_save_path) - if self.task.result_subtitle_save_path: + if self.task.result_subtitle_save_path and Path(self.task.result_subtitle_save_path).exists(): preview_subtitle_path = Path(self.task.result_subtitle_save_path) + # The original sub might be word-split and not full sentence sub. + # elif self.task.original_subtitle_save_path and Path(self.task.original_subtitle_save_path).exists(): + # preview_subtitle_path = Path(self.task.original_subtitle_save_path) + else: + # Open file dialog + subtitle_formats = [f"*.{fmt.value}" for fmt in SupportedSubtitleFormats] + filter_str = f"{self.tr('字幕文件')} ({' '.join(subtitle_formats)})" + file, _ = QFileDialog.getOpenFileName( self, self.tr("选择字幕文件"), cfg.last_open_dir.value, filter_str) + if file and Path(file).exists(): + preview_subtitle_path = Path(file) + else: + return + if preview_subtitle_path.exists(): self.subtitle_window = QWidget() self.subtitle_window.setWindowTitle(self.tr("字幕预览")) diff --git a/app/view/setting_interface.py b/app/view/setting_interface.py index 551cb19..dccca91 100644 --- a/app/view/setting_interface.py +++ b/app/view/setting_interface.py @@ -7,7 +7,7 @@ from qfluentwidgets import (SettingCardGroup, SwitchSettingCard, OptionsSettingCard, PushSettingCard, HyperlinkCard, PrimaryPushSettingCard, ScrollArea, ComboBoxSettingCard, ExpandLayout, CustomColorSettingCard, RangeSettingCard, - setTheme, setThemeColor, RangeSettingCard, MessageBox) + setTheme, setThemeColor ) from app.components.WhisperAPISettingDialog import WhisperAPISettingDialog from app.config import VERSION, YEAR, AUTHOR, HELP_URL, FEEDBACK_URL, RELEASE_URL @@ -230,6 +230,14 @@ def __init__(self, parent=None): self.subtitleGroup ) + self.VerticalOffsetCard = RangeSettingCard( + cfg.vertical_offset, + FIF.MOVE, + self.tr('Vertical Offset for Subtitles'), + self.tr('In pixels, the vertical offset to apply to all subtitle positions.'), + self.subtitleGroup + ) + # 保存配置 self.saveGroup = SettingCardGroup(self.tr("保存配置"), self.scrollWidget) self.savePathCard = PushSettingCard( @@ -369,6 +377,7 @@ def __initLayout(self): self.subtitleGroup.addSettingCard(self.enableSubtitleSentenceMinimumTimeCard) self.subtitleGroup.addSettingCard(self.SubtitleSentenceMinimumTimeCard) self.subtitleGroup.addSettingCard(self.SubtitleTimeOffsetCard) + self.subtitleGroup.addSettingCard(self.VerticalOffsetCard) self.saveGroup.addSettingCard(self.savePathCard) self.personalGroup.addSettingCard(self.themeCard) @@ -425,6 +434,8 @@ def __connectSignalToSlot(self): self.internetTranslateCard.checkedChanged.connect(signalBus.on_internet_translation_changed) self.internetTranslateMethodCard.comboBox.currentTextChanged.connect(signalBus.on_internet_translation_method_changed) self.targetLanguageCard.comboBox.currentTextChanged.connect(signalBus.on_target_language_changed) + self.softSubtitleCard.checkedChanged.connect(signalBus.on_soft_subtitle_changed) + self.needVideoCard.checkedChanged.connect(signalBus.on_need_video_changed) # self.languageCard.comboBox.currentTextChanged.connect(signalBus.on_language_changed) signalBus.subtitle_optimization_changed.connect(self.subtitleCorrectCard.setChecked) @@ -434,7 +445,7 @@ def __connectSignalToSlot(self): signalBus.internet_translation_method_changed.connect(self.internetTranslateMethodCard.comboBox.setCurrentText) signalBus.target_language_changed.connect(self.targetLanguageCard.comboBox.setCurrentText) signalBus.language_changed.connect(self.languageCard.comboBox.setCurrentText) - + def show_whisper_settings(self): """显示Whisper设置对话框""" if self.transcribeModelCard.comboBox.currentText() == TranscribeModelEnum.WHISPER.value: diff --git a/app/view/task_creation_interface.py b/app/view/task_creation_interface.py index e7f932f..dbe86b9 100644 --- a/app/view/task_creation_interface.py +++ b/app/view/task_creation_interface.py @@ -433,7 +433,7 @@ def _is_valid_url(self, url): return False def _process_file(self, file_path): - self.create_task_thread = CreateTaskThread(file_path, Task.Type.SUBTITLE) + self.create_task_thread = CreateTaskThread(file_path, Task.Type.SUBTITLE, cfg.soft_subtitle.value) self.create_task_thread.finished.connect(self.on_create_task_finished) self.create_task_thread.progress.connect(self.on_create_task_progress) self.create_task_thread.start() @@ -448,7 +448,7 @@ def _process_url(self, url): duration=5000, parent=self ) - self.create_task_thread = CreateTaskThread(url, Task.Type.URL) + self.create_task_thread = CreateTaskThread(url, Task.Type.URL, cfg.soft_subtitle.value) self.create_task_thread.finished.connect(self.on_create_task_finished) self.create_task_thread.progress.connect(self.on_create_task_progress) self.create_task_thread.error.connect(self.on_create_task_error) diff --git a/app/view/video_synthesis_interface.py b/app/view/video_synthesis_interface.py index 9914c21..a1d5918 100644 --- a/app/view/video_synthesis_interface.py +++ b/app/view/video_synthesis_interface.py @@ -7,15 +7,15 @@ from ..common.config import cfg from PyQt5.QtCore import Qt, pyqtSignal -from PyQt5.QtGui import QDropEvent +from PyQt5.QtGui import QDropEvent, QColor from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QApplication, QFileDialog) -from qfluentwidgets import (CardWidget, ComboBox, LineEdit, BodyLabel, - InfoBar, InfoBarPosition, ProgressBar, PushButton) +from qfluentwidgets import (CardWidget, LineEdit, BodyLabel, SwitchButton, IndicatorPosition, + InfoBar, InfoBarPosition, ProgressBar, PushButton, Slider) from app.core.thread.create_task_thread import CreateTaskThread from app.core.thread.video_synthesis_thread import VideoSynthesisThread -from ..core.entities import SupportedVideoFormats, SupportedSubtitleFormats +from ..core.entities import SupportedVideoFormats, SupportedSubtitleFormats, SupportedImageFormats from ..core.entities import Task current_dir = Path(__file__).parent.parent @@ -35,6 +35,7 @@ def __init__(self, parent=None): self.set_value() self.setup_signals() self.task = None + self.portrait_background = None def setup_ui(self): self.main_layout = QVBoxLayout(self) @@ -71,7 +72,58 @@ def setup_ui(self): self.video_layout.addWidget(self.video_input) self.video_layout.addWidget(self.video_button) self.config_layout.addLayout(self.video_layout) - + + # 附加选项 + self.options_layout = QHBoxLayout() + self.option_soft_subtitle = SwitchButton(self.tr("软字幕"), self, indicatorPos=IndicatorPosition.RIGHT) + self.option_soft_subtitle.setOnText(self.tr("硬字幕")) + self.option_portrait = SwitchButton(self.tr("横屏字幕"), self, indicatorPos=IndicatorPosition.RIGHT) + self.option_portrait.setOnText(self.tr("竖屏字幕")) + self.option_portrait.setDisabled(True) + self.option_portrait_background = PushButton(self.tr("背景:无"), self) + self.option_portrait_background.setDisabled(True) + + # 字幕垂直偏移 + self.option_vertical_offset_label = BodyLabel(self.tr("垂直偏移量 (px): 100"),self) + self.option_vertical_offset_label.setFixedWidth(130) + self.set_bodylabel_disabled(self.option_vertical_offset_label, True) + self.option_vertical_offset = Slider(Qt.Orientation.Vertical, self) + self.option_vertical_offset.setRange(-500, 500) + self.option_vertical_offset.setValue(0) + self.option_vertical_offset.setDisabled(True) + + # 竖屏时视频大小 + self.option_zoom_video_label = BodyLabel(self.tr("竖屏视频大小: 100%"),self) + self.option_zoom_video_label.setFixedWidth(130) + self.set_bodylabel_disabled(self.option_zoom_video_label, True) + self.option_zoom_video = Slider(Qt.Orientation.Vertical, self) + + self.option_zoom_video.setRange(-300, -10) + self.option_zoom_video.setValue(-100) + self.option_zoom_video.setDisabled(True) + + # 竖屏时字幕大小 + self.option_zoom_subtitle_label = BodyLabel(self.tr("竖屏字幕大小: 100%"),self) + self.option_zoom_subtitle_label.setFixedWidth(130) + self.set_bodylabel_disabled(self.option_zoom_subtitle_label, True) + self.option_zoom_subtitle = Slider(Qt.Orientation.Vertical, self) + self.option_zoom_subtitle.setRange(-300, -10) + self.option_zoom_subtitle.setValue(-100) + self.option_zoom_subtitle.setDisabled(True) + + + self.options_layout.addWidget(self.option_soft_subtitle) + self.options_layout.addWidget(self.option_portrait) + self.options_layout.addWidget(self.option_portrait_background) + self.options_layout.addWidget(self.option_vertical_offset_label) + self.options_layout.addWidget(self.option_vertical_offset) + self.options_layout.addWidget(self.option_zoom_video_label) + self.options_layout.addWidget(self.option_zoom_video) + self.options_layout.addWidget(self.option_zoom_subtitle_label) + self.options_layout.addWidget(self.option_zoom_subtitle) + self.options_layout.addStretch(1) + self.config_layout.addLayout(self.options_layout) + self.main_layout.addWidget(self.config_card) # 合成按钮和打开文件夹按钮 @@ -97,6 +149,68 @@ def setup_ui(self): self.bottom_layout.addWidget(self.status_label) # 状态标签使用固定宽度 self.main_layout.addLayout(self.bottom_layout) + def on_portrait_background_clicked(self): + # Open file dialog to select background image + image_formats = {f"*.{fmt.value}" for fmt in SupportedImageFormats} + file_str, _ = QFileDialog.getOpenFileName(self, self.tr("选择背景图片"), cfg.last_open_dir.value, ' '.join(image_formats)) + if file_str: + file_path = Path(file_str) + if file_path.exists(): + self.option_portrait_background.setText(f"背景:{file_path.name}") + # self.option_portrait_background.setFixedWidth(200) + self.portrait_background = file_str + else: + InfoBar.error( + self.tr("错误"), + self.tr("无效的文件路径"), + duration=3000, + position=InfoBarPosition.TOP, + parent=self + ) + else: # User canceled the file selection + self.option_portrait_background.setText("背景:无") + self.option_portrait_background.setFixedWidth(100) + self.portrait_background = None + + + def set_bodylabel_disabled(self, label: BodyLabel, disable: bool): + if disable: + label.setTextColor(light=QColor(128,128,128), dark=QColor(128,128,128)) + else: + label.setTextColor(light=QColor(0,0,0), dark=QColor(255,255,255)) + + + def on_soft_subtitle_toggled(self, checked): + # checked means hard subtitle + self.option_portrait.setDisabled(not checked) + if self.option_portrait.isChecked(): + # 竖屏 + self.option_portrait_background.setDisabled(not checked) + self.option_zoom_video.setDisabled(not checked) + self.set_bodylabel_disabled(self.option_zoom_video_label, not checked ) + self.option_zoom_subtitle.setDisabled(not checked) + self.set_bodylabel_disabled(self.option_zoom_subtitle_label, not checked ) + + self.option_vertical_offset.setDisabled(not checked) + self.set_bodylabel_disabled(self.option_vertical_offset_label, not checked) + + def on_vertical_offset_changed(self, offset): + self.option_vertical_offset_label.setText(self.tr("垂直偏移量 (px): ") + str(offset) ) + + def on_zoom_video_changed(self, zoom): + self.option_zoom_video_label.setText(self.tr(f"竖屏视频大小: {-zoom}%")) + + def on_zoom_subtitle_changed(self, zoom): + self.option_zoom_subtitle_label.setText(self.tr(f"竖屏字幕大小: {-zoom}%")) + + def on_portrait_toggled(self, checked): + self.option_portrait_background.setDisabled(not checked) + self.option_zoom_video.setDisabled(not checked) + self.set_bodylabel_disabled(self.option_zoom_video_label, not checked ) + self.option_zoom_subtitle.setDisabled(not checked) + self.set_bodylabel_disabled(self.option_zoom_subtitle_label, not checked ) + + def setup_style(self): self.subtitle_input.focusOutEvent = lambda e: super(LineEdit, self.subtitle_input).focusOutEvent(e) self.subtitle_input.paintEvent = lambda e: super(LineEdit, self.subtitle_input).paintEvent(e) @@ -135,6 +249,14 @@ def setup_signals(self): self.synthesize_button.clicked.connect(self.process) self.open_folder_button.clicked.connect(self.open_video_folder) self.open_work_folder_button.clicked.connect(self.open_work_folder) + + # 字幕选项相关信号 + self.option_soft_subtitle.checkedChanged.connect(self.on_soft_subtitle_toggled) + self.option_portrait.checkedChanged.connect(self.on_portrait_toggled) + self.option_portrait_background.clicked.connect(self.on_portrait_background_clicked) + self.option_vertical_offset.valueChanged.connect(self.on_vertical_offset_changed) + self.option_zoom_video.valueChanged.connect(self.on_zoom_video_changed) + self.option_zoom_subtitle.valueChanged.connect(self.on_zoom_subtitle_changed) def set_value(self): pass @@ -180,7 +302,21 @@ def create_task(self): ) return None - self.task = CreateTaskThread.create_video_synthesis_task(subtitle_file, video_file) + soft_sub = not self.option_soft_subtitle.isChecked() # when checked, it's hard sub + self.task = CreateTaskThread.create_video_synthesis_task(subtitle_file, video_file, soft_sub) + if not soft_sub: + # Hard coded subtitle + if self.option_portrait.isChecked(): # portrait sub + self.task.portrait = True + self.task.portrait_background = self.portrait_background + # They are negative numbers from -300 to -10 + self.task.zoom_video = abs(self.option_zoom_video.value()) + self.task.zoom_subtitle = abs(self.option_zoom_subtitle.value()) + else: + self.task.portrait = False + + self.task.vertical_offset = self.option_vertical_offset.value() + return self.task def set_task(self, task: Task): @@ -199,7 +335,9 @@ def process(self): if not self.task: self.task = None self.create_task() - if self.task.file_path != self.video_input.text() or self.task.result_subtitle_save_path != self.subtitle_input.text(): + + if self.task.file_path != str(Path(self.video_input.text())) \ + or self.task.original_subtitle_save_path != str(Path(self.subtitle_input.text())): self.task = None self.create_task() diff --git a/resource/translations/VideoCaptioner_en_US.qm b/resource/translations/VideoCaptioner_en_US.qm index 59ed81d..567e240 100644 Binary files a/resource/translations/VideoCaptioner_en_US.qm and b/resource/translations/VideoCaptioner_en_US.qm differ diff --git a/resource/translations/VideoCaptioner_en_US.ts b/resource/translations/VideoCaptioner_en_US.ts index a89d8bd..68104f1 100644 --- a/resource/translations/VideoCaptioner_en_US.ts +++ b/resource/translations/VideoCaptioner_en_US.ts @@ -4,36 +4,23 @@ BatchProcessInterface - + 批量处理 Batch Processing - + 添加视频文件 Add Video Files - + 清空任务 Clear Tasks - - - - - - 视频加字幕 - Add Subtitles to Video - - - - 音视频转录 - Audio/Video Transcription - - + 开始处理 Start Processing @@ -48,167 +35,167 @@ - + 无法清空 Cannot Clear - + 正在处理的任务无法清空 Cannot clear tasks that are being processed - + 已清空 Cleared - + 已清空所有任务 All tasks have been cleared - + 开始批量处理任务 Start Batch Processing Tasks - + 已取消批量处理 Batch Processing Cancelled - + 警告 Warning - + 没有可处理的任务 No tasks to process - + 已取消 Cancelled - + 任务完成 Task Completed - + 任务已完成 Task has been completed - + Program exiting in 1 minute - + All jobs are done. This program is going to be closed. - + Suspending in 1 minute - + All jobs are done. The computer is going to be suspended. - + Shutting Down in 1 minute - + All jobs are done. The computer is shutting down. - + 全部完成 All Completed - + 所有任务已处理完成 All tasks have been processed - + 选择文件 Select File - + 添加失败 Add Failed - + 该文件已存在于任务列表中 This file already exists in the task list - + 添加成功 Added Successfully - + 已添加视频: Video added: - + 无法删除 Cannot Delete - + 正在处理的任务无法删除 Cannot delete tasks that are being processed - + 删除成功 Deleted Successfully - + 已删除任务: Task deleted: - + 请拖入视频文件 Please drag in video files - + 格式错误 Format Error - + 请拖入音频或视频文件 Please drag in audio or video files - + 任务出错 Task Error - + 任务出错: Task Error: @@ -224,22 +211,22 @@ CreateTaskThread - + 创建任务失败 Failed to create task - + 创建任务完成 Task created - + 正在获取视频信息 Getting video information - + 下载视频完成 Video downloading finished @@ -915,185 +902,195 @@ - + + Vertical Offset for Subtitles + + + + + In pixels, the vertical offset to apply to all subtitle positions. + + + + 保存配置 Save Settings - + 工作文件夹 Working Folder - + 工作目录路径 Working directory path - + 个性化 Personalization - + 应用主题 Application Theme - + 更改应用程序的外观 Change application appearance - + 浅色 Light - + 深色 Dark - - + + 使用系统设置 Use System Settings - + 主题颜色 Theme Color - + 更改应用程序的主题颜色 Change application theme color - + 界面缩放 Interface Scaling - + 更改小部件和字体的大小 Change size of widgets and fonts - + 语言 Language - + 设置您偏好的界面语言 Set your preferred interface language - - + + 关于 About - + 打开帮助页面 Open Help Page - + 帮助 Help - - + + 提供反馈 Provide Feedback - + 检查更新 Check for Updates - + 版权所有 Copyright - + 版本 Version - - + + 错误 Error - + 请先选择Whisper转录模型 Please choose Whisper transcribe model - + 请输入正确的 API Base, 含有 /v1 Please enter a valid API Base containing /v1 - + 正在检查... Checking... - + 检查连接 Check Connection - - + + LLM 连接测试错误 LLM connection test error - + 获取模型列表成功: Successfully fetched model list: - + 一共 Totally - + 个模型 models - + LLM 连接测试成功 LLM connection test successful - + 更新成功 Update Successful - + 配置将在重启后生效 Configuration will take effect after restart - + 选择文件夹 Select Folder @@ -1228,12 +1225,12 @@ - + 发现新功能并了解有关VideoCaptioner的使用技巧 Discover new features and learn tips about using VideoCaptioner - + 提供反馈帮助我们改进VideoCaptioner Provide feedback to help us improve VideoCaptioner @@ -1485,37 +1482,37 @@ Detail settings are in the 'Settings' on the lower left corner.Start Optimizing Subtitle... - + 字幕断句... Subtitle Sentence Breaking... - + 总结字幕... Subtitle Summerizing... - + 优化+翻译... Optimizing + Translating... - + 批量翻译单句字幕... Batch translating subtitles by single sentence... - + 优化/翻译完成 Optimizing / Translation finished - + 优化失败 Optimization failed - + {0}% 处理字幕 {0}% processing subtitle @@ -1538,12 +1535,12 @@ Detail settings are in the 'Settings' on the lower left corner.Start translating subtitles with single sentence method - + 开始合成视频 Synthesizing Started - + 处理完成 Process Finished @@ -2090,175 +2087,241 @@ Detail settings are in the 'Settings' on the lower left corner. TaskInfoCard - + 未选择视频 No video selected - + 画质 Quality - + 文件大小 File Size - + 时长 Duration - + 视频码 Video Codec - + 音频码 Audio Codec - + 竖屏 + Portrait + + + + 横屏 + Landscape + + + + 背景:无 + BG: None + + + + 打开文件夹 Open Folder - + 画质: Quality: - + 大小: Size: - + 时长: Duration: - + 视频码 Video Codec - + 音频码 Audio Codec - - 无 - None - - - - 字幕优化 - Subtitle Optimization - - - - 转录模型: - Transcription Model: + + 竖屏背景: + Portrait BG: - + + 文件: File: - - 字幕策略: - Subtitle Strategy: - - - + 任务状态: Task Status: - + 删除任务 Delete Task - + 重新处理 Reprocess - - 取消任务 - Cancel Task - - - + 已取消 Cancelled - + 任务已取消 Task has been cancelled - - - + + + 警告 Warning - + + 翻译方式:智能多线程优化+翻译,目标: + Translate Method: AI LLM multi-thread optimizing+translating, Target: + + + + 翻译方式:智能单线程单句翻译,目标: + Translate Method: AI single sentence translating. Target: + + + + ,使用的LLM 模型: + , LLM in use: + + + + 字幕类型:软字幕 + Subtitling Method: Soft + + + + 字幕类型:硬字幕 + Subtitling Method: Hard + + + + 竖屏模式:开启 + Portrait: On + + + + 任务类型: + Task Type: + + + + 转录模型: + Transcribe Model: + + + + 选择背景图片 + Select Background Image + + + + Image Files ( + + + + + 文件不存在 + File Not Exists + + + + 请重新选择 + Please choose again + + + + 背景: + BG: + + + + 停止任务 + Stop Task + + + + 选择字幕文件 + Choose Subtitle File + + + 该任务已完成 This task has been completed - + 转录失败 Transcription failed - + 预览字幕 Preview Subtitles - - 字幕翻译 - Subtitle Translate - - - + 打开字幕(双击) Open Subtitles (Double Click) - + 字幕预览 Subtitle Preview - + 字幕文件不存在 Subtitle file does not exist - + 未开始转录 Not Start Yet - + 任务类型错误 Task Type Error - + 任务未开始 Task not started @@ -2302,12 +2365,12 @@ Detail settings are in the 'Settings' on the lower left corner.Invalid transcribe model: - + 转录完成 Transcription Complete - + 转录失败 Transcribing Failed @@ -2432,121 +2495,187 @@ Detail settings are in the 'Settings' on the lower left corner. VideoSynthesisInterface - + 选择或者拖拽字幕文件 Select or drag subtitle file - - + + 浏览 Browse - + 选择或者拖拽视频文件 Select or drag video file - + 开始合成 Start Synthesis - + 打开视频文件夹 Open Video Folder - + 就绪 Ready - + 选择字幕文件 Select Subtitle File - + 选择视频文件 Select Video File - - - + + + + 错误 Error - + + 软字幕 + Soft Sub + + + + 硬字幕 + Hard Sub + + + + 横屏字幕 + Landscape + + + + 竖屏字幕 + Portrait + + + + 背景:无 + BG: None + + + + 垂直偏移量 (px): 100 + V Offeset (px): 100 + + + + 竖屏视频大小: 100% + Video Zoom: 100% + + + + 竖屏字幕大小: 100% + Subtitle Zoom 100% + + + + 选择背景图片 + Select BG Image + + + + 无效的文件路径 + Invalid file path + + + + 垂直偏移量 (px): + V Offset (px): + + + + 竖屏视频大小: {-zoom}% + Video Size: {-zoom}% + + + + 竖屏字幕大小: {-zoom}% + Subtitle Size: {-zoom}% + + + 请选择字幕文件和视频文件 Please select subtitle file and video file - + 无法创建任务 Cannot create task - + 成功 Success - + 视频合成已完成 Video synthesis completed - + 警告 Warning - + 没有可用的视频文件夹 No video folder available - - + + 导入成功 Import Successful - + 字幕文件已放入输入框 Subtitle file has been placed in input box - + 视频文件已输入框 Video file has been placed in input box - + 格式错误 Format Error - + 请拖入视频或者字幕文件 Please drag in video or subtitle file - + 字幕文件 Subtitle File - + 视频文件 Video File - + Open Work Dir @@ -2555,7 +2684,7 @@ Detail settings are in the 'Settings' on the lower left corner.VideoSynthesisThread - + 合成完成 Synthesis Done @@ -2570,7 +2699,7 @@ Detail settings are in the 'Settings' on the lower left corner. - + 视频合成失败 Video synthesizing failed @@ -2759,17 +2888,17 @@ Detail settings are in the 'Settings' on the lower left corner. qoCreateTask - + 【翻译字幕】 TranslatedSub_ - + 【修正字幕】 FixedSub_ - + 【字幕】 sub_ @@ -2778,170 +2907,189 @@ Detail settings are in the 'Settings' on the lower left corner.qoEnums - Original Only + Create Subtitle from Audio/Video + Create Soft Subtitle Video + + + + + Create Hard Subtitle Video + + + + + Original Only + + + + Translated Only - + Original on Top - + Translated on Top - + Google Translate - + Nothing - + Exit The Program - + Shutdown The Computer - + Suspend The Computer - + Canceled - + Completed - + Downloading - + Failed - + Generating - + Optimizing - + Pending - + Synthesizing - + Transcoding - + Translating - + Waiting for audio transcoding - + Waiting for optimization - + Waiting for video synthesis - + Waiting for transcripting - + File Import - URL Import - - Optimize + + Optimize + Translate Subtitles - - Subtitle + + Add Subtitle To Video - - Synthesis + + Combine Subtitle with Video - - Transcribe + + Get Subtitle From Video/Audio + + + + + Download Video from URL then Add Subtitle qoVideo - + 输入文件不存在 Input File Does Not Exist - + 字幕文件不存在 Subtitle file does not exist - + 正在合成 Synthesizing - + 合成完成 Synthesis Done diff --git a/resource/translations/VideoCaptioner_zh_CN.qm b/resource/translations/VideoCaptioner_zh_CN.qm index 14a3047..d0ccf5e 100644 Binary files a/resource/translations/VideoCaptioner_zh_CN.qm and b/resource/translations/VideoCaptioner_zh_CN.qm differ diff --git a/resource/translations/VideoCaptioner_zh_CN.ts b/resource/translations/VideoCaptioner_zh_CN.ts index 6b6ae13..833c68f 100644 --- a/resource/translations/VideoCaptioner_zh_CN.ts +++ b/resource/translations/VideoCaptioner_zh_CN.ts @@ -4,36 +4,23 @@ BatchProcessInterface - + 批量处理 - + 添加视频文件 - + 清空任务 - - - - - - 视频加字幕 - - - - - 音视频转录 - - - + 开始处理 @@ -48,167 +35,167 @@ 所有任务完成后, - + 无法清空 - + 正在处理的任务无法清空 - + 已清空 - + 已清空所有任务 - + 警告 - + 没有可处理的任务 - + 开始批量处理任务 - + 已取消 - + 已取消批量处理 - + 任务完成 - + 任务已完成 - + 任务出错 - + 任务出错: - + Program exiting in 1 minute 程序在1分钟后退出 - + All jobs are done. This program is going to be closed. 所有工作都已完成,程序会自动关闭。 - + Suspending in 1 minute 一分钟后挂机 - + All jobs are done. The computer is going to be suspended. 所有工作都已完成。电脑会自动进入睡眠状态。 - + Shutting Down in 1 minute 一分钟后关机 - + All jobs are done. The computer is shutting down. 所有工作都已完成,电脑会自动关机。 - + 全部完成 全部完成 - + 所有任务已处理完成 所有任务都已处理完成 - + 选择文件 - + 添加失败 - + 该文件已存在于任务列表中 - + 添加成功 - + 已添加视频: - + 无法删除 - + 正在处理的任务无法删除 - + 删除成功 - + 已删除任务: - + 请拖入视频文件 - + 请拖入音频或视频文件 - + 格式错误 @@ -224,22 +211,22 @@ CreateTaskThread - + 创建任务失败 - + 创建任务完成 - + 正在获取视频信息 - + 下载视频完成 @@ -850,8 +837,8 @@ - + 检查连接 @@ -1048,191 +1035,201 @@ In milliseconds, the offset to apply to all subtitle timings. - 以千分之一秒为单位,调整整个字幕的时间轴 + 以千分之一秒为单位,调整整个字幕的时间轴。 - + + Vertical Offset for Subtitles + 垂直方向字幕的位移 + + + + In pixels, the vertical offset to apply to all subtitle positions. + 以像素为单位,对所有字幕在垂直方向上的位移。 + + + 保存配置 - + 工作文件夹 - + 工作目录路径 - + 个性化 - + 应用主题 - + 更改应用程序的外观 - + 浅色 - + 深色 - - + + 使用系统设置 - + 主题颜色 - + 更改应用程序的主题颜色 - + 界面缩放 - + 更改小部件和字体的大小 - + 语言 - + 设置您偏好的界面语言 - - + + 关于 - + 打开帮助页面 - + 帮助 - + 发现新功能并了解有关VideoCaptioner的使用技巧 - - + + 提供反馈 - + 提供反馈帮助我们改进VideoCaptioner - + 检查更新 - + 版权所有 - + 版本 - - + + 错误 - + 请先选择Whisper转录模型 - + 更新成功 - + 配置将在重启后生效 - + 选择文件夹 - + 请输入正确的 API Base, 含有 /v1 - + 正在检查... - - + + LLM 连接测试错误 - + 获取模型列表成功: - + 一共 - + 个模型 - + LLM 连接测试成功 @@ -1483,37 +1480,37 @@ - + 字幕断句... - + 总结字幕... - + 优化+翻译... - + 批量翻译单句字幕... - + 优化/翻译完成 - + 优化失败 - + {0}% 处理字幕 @@ -1536,12 +1533,12 @@ - + 开始合成视频 - + 处理完成 @@ -2088,175 +2085,241 @@ TaskInfoCard - + 未选择视频 - + 画质 - + 文件大小 - + 时长 - + 视频码 - + 音频码 - + + 竖屏 + + + + + 横屏 + + + + + 背景:无 + + + + 预览字幕 - - + + 打开文件夹 - + 未开始转录 - + 画质: - + 大小: - + 时长: - + 视频码 - + 音频码 - - 无 + + 翻译方式:智能多线程优化+翻译,目标: - - 字幕优化 + + 翻译方式:智能单线程单句翻译,目标: - - 字幕翻译 + + ,使用的LLM 模型: - - 转录模型: + + 字幕类型:软字幕 - - 文件: + + 字幕类型:硬字幕 + + + + + 竖屏模式:开启 - - 字幕策略: + + 任务类型: - + + 转录模型: + + + + + 停止任务 + + + + + 选择字幕文件 + + + + + + 文件: + + + + + 竖屏背景: + + + + 任务状态: - - 打开字幕(双击) + + 选择背景图片 - - 删除任务 + + Image Files ( + 图片文件( + + + + 文件不存在 - - 重新处理 + + 请重新选择 - - 取消任务 + + 背景: - + + 打开字幕(双击) + + + + + 删除任务 + + + + + 重新处理 + + + + 字幕预览 - - - + + + 警告 - + 字幕文件不存在 - + 已取消 - + 任务已取消 - + 该任务已完成 - + 任务类型错误 - + 任务未开始 - + 转录失败 @@ -2300,12 +2363,12 @@ - + 转录完成 - + 转录失败 @@ -2430,121 +2493,187 @@ VideoSynthesisInterface - + 字幕文件 - + 选择或者拖拽字幕文件 - - + + 浏览 - + 视频文件 - + 选择或者拖拽视频文件 - + 开始合成 - + 打开视频文件夹 - + Open Work Dir 打开工作目录 - + 就绪 - + 选择字幕文件 - + 选择视频文件 - - - + + + + 错误 - + + 软字幕 + + + + + 硬字幕 + + + + + 横屏字幕 + + + + + 竖屏字幕 + + + + + 背景:无 + + + + + 垂直偏移量 (px): 100 + + + + + 竖屏视频大小: 100% + + + + + 竖屏字幕大小: 100% + + + + + 选择背景图片 + + + + + 无效的文件路径 + + + + + 垂直偏移量 (px): + + + + + 竖屏视频大小: {-zoom}% + + + + + 竖屏字幕大小: {-zoom}% + + + + 请选择字幕文件和视频文件 - + 无法创建任务 - + 成功 - + 视频合成已完成 - + 警告 - + 没有可用的视频文件夹 - - + + 导入成功 - + 字幕文件已放入输入框 - + 视频文件已输入框 - + 格式错误 - + 请拖入视频或者字幕文件 @@ -2553,7 +2682,7 @@ VideoSynthesisThread - + 合成完成 @@ -2568,7 +2697,7 @@ 等待视频合成 - + 视频合成失败 @@ -2757,17 +2886,17 @@ qoCreateTask - + 【翻译字幕】 - + 【修正字幕】 - + 【字幕】 @@ -2776,170 +2905,189 @@ qoEnums + Create Subtitle from Audio/Video + 从视频音频制作字幕 + + + + Create Soft Subtitle Video + 视频加入软字幕 + + + + Create Hard Subtitle Video + 视频加入硬字幕 + + + Original Only 只留原文 - + Translated Only 只留译文 - + Original on Top 原文在上 - + Translated on Top 译文在上 - + Google Translate 谷歌翻译 - + Nothing 没事 - + Exit The Program 退出程序 - + Shutdown The Computer 关机 - + Suspend The Computer 机器睡眠 - + Canceled 已取消 - + Completed 已完成 - + Downloading 正在下载 - + Failed 已失败 - + Generating 正在产生 - + Optimizing 正在优化 - + Pending 等待 - + Synthesizing 正在合成 - + Transcoding 正在转码 - + Translating 正在翻译 - + Waiting for audio transcoding 等待语音转码 - + Waiting for optimization 等待优化 - + Waiting for video synthesis 等待合成 - + Waiting for transcripting 等待转录 - + File Import 接受文件 - URL Import 接受网址 - - Optimize - 优化 + + Optimize + Translate Subtitles + 优化+翻译字幕 - - Subtitle - 字幕 + + Add Subtitle To Video + 视频添加字幕 - - Synthesis - 合成 + + Combine Subtitle with Video + 视频合成字幕 - - Transcribe - 转录 + + Get Subtitle From Video/Audio + 从音频视频提取字幕 + + + + Download Video from URL then Add Subtitle + 从网络下载视频然后加入字幕 qoVideo - + 输入文件不存在 - + 字幕文件不存在 - + 正在合成 - + 合成完成