From 260e673b86907ba810fb2b55e4a777b48266c8f6 Mon Sep 17 00:00:00 2001 From: Philip Wang Date: Sat, 25 Jan 2025 08:27:37 -0500 Subject: [PATCH] Many fixes. 1.8 --- activate.bat | 1 - app/common/config.py | 11 +- app/common/enums.py | 105 ++++++++++------ app/common/signal_bus.py | 3 + app/components/EnumComboBoxSettingCard.py | 64 +++------- app/core/bk_asr/ASRData.py | 55 +++++---- app/core/entities.py | 42 +++++-- app/core/thread/create_task_thread.py | 99 +++++++++------ .../thread/subtitle_optimization_thread.py | 54 ++------ app/core/thread/transcript_thread.py | 78 +++++++++--- app/core/utils/logger.py | 14 +++ app/view/batch_process_interface.py | 68 ++++++----- app/view/home_interface.py | 17 +-- app/view/setting_interface.py | 8 +- app/view/subtitle_optimization_interface.py | 21 ++-- app/view/subtitle_style_interface.py | 25 ++-- app/view/task_creation_interface.py | 31 ++++- app/view/transcription_interface.py | 115 ++++++++++-------- main.py | 8 +- run.bat | 2 + 20 files changed, 469 insertions(+), 352 deletions(-) delete mode 100644 activate.bat create mode 100644 run.bat diff --git a/activate.bat b/activate.bat deleted file mode 100644 index 98cb4d9..0000000 --- a/activate.bat +++ /dev/null @@ -1 +0,0 @@ -.\.venv\Scripts\activate.bat \ No newline at end of file diff --git a/app/common/config.py b/app/common/config.py index ce3de55..7aaf9b4 100644 --- a/app/common/config.py +++ b/app/common/config.py @@ -1,13 +1,14 @@ # coding:utf-8 from enum import Enum -from PyQt5.QtCore import QLocale, QObject +from PyQt5.QtCore import QLocale from PyQt5.QtGui import QColor from qfluentwidgets import (qconfig, QConfig, ConfigItem, OptionsConfigItem, BoolValidator, OptionsValidator, RangeConfigItem, RangeValidator, Theme, FolderValidator, ConfigSerializer, EnumSerializer) from app.config import WORK_PATH, SETTINGS_PATH +from .enums import EnumExSerializer, EnumOptionsValidator from ..core.entities import ( TargetLanguageEnum, TranscribeModelEnum, @@ -20,9 +21,6 @@ SubtitleLayoutEnum, InternetTranslateEnum, ) -from ..components.EnumComboBoxSettingCard import EnumExSerializer, EnumOptionsValidator - -qoConfig = QObject() # qo means QObject class Language(Enum): """ 软件语言 """ @@ -168,9 +166,10 @@ class Config(QConfig): # ------------------- 字幕样式配置 ------------------- subtitle_style_name = ConfigItem("SubtitleStyle", "StyleName", "default") + subtitle_layout = OptionsConfigItem( "SubtitleStyle", "Layout", - SubtitleLayoutEnum.ONLY_TRANSLATE.name, + SubtitleLayoutEnum.ONLY_TRANSLATE, EnumOptionsValidator(SubtitleLayoutEnum), EnumExSerializer(SubtitleLayoutEnum) ) @@ -240,7 +239,7 @@ class Config(QConfig): todo_when_done = OptionsConfigItem( "All", "ToDo_When_Done", - "NOTHING", + TodoWhenDoneEnum.NOTHING, EnumOptionsValidator(TodoWhenDoneEnum), EnumExSerializer(TodoWhenDoneEnum) ) diff --git a/app/common/enums.py b/app/common/enums.py index a89a217..269f5ae 100644 --- a/app/common/enums.py +++ b/app/common/enums.py @@ -1,46 +1,81 @@ # Set the enums to new translated values +from enum import Enum from PyQt5.QtCore import QObject +from qfluentwidgets import ConfigValidator, ConfigSerializer from ..core.entities import SubtitleLayoutEnum, InternetTranslateEnum, TodoWhenDoneEnum, Task, BatchTaskTypeEnum +class EnumOptionsValidator(ConfigValidator): + """ Enum Options validator """ + + def __init__(self, enumClass: Enum): + if not enumClass or len(enumClass) == 0: + raise ValueError("The `enums` can't be empty.") + + if issubclass(enumClass, Enum): + self.enumClass = enumClass + else: + self.enums = None + + def validate(self, enum): + return enum in self.enumClass + + def correct(self, enum): + return enum if self.validate(enum) else list(self.enumClass)[0] + +class EnumExSerializer(ConfigSerializer): + """ enumeration class serializer for multi-language """ + # It use names to serialize instead of values + + def __init__(self, enumClass): + self.enumClass = enumClass + + def serialize(self, item): + # From configItem.value to name + return item.name + + def deserialize(self, name): + # From name to configItem.value, which is an Enum + return self.enumClass[name] + def Enums_Translate(): qoEnums = QObject() - BatchTaskTypeEnum.TRANSCRIBE._value_ = qoEnums.tr("Create Transcription from Audio/Video") - BatchTaskTypeEnum.TRANSLATE._value_ = qoEnums.tr("Transcribe + Translate Audio/Video") - BatchTaskTypeEnum.SOFT._value_ = qoEnums.tr("Create Soft Subtitle Video") - BatchTaskTypeEnum.HARD._value_ = qoEnums.tr("Create Hard Subtitle Video") + BatchTaskTypeEnum.TRANSCRIBE.setValue( qoEnums.tr("Create Transcription from Audio/Video") ) + BatchTaskTypeEnum.TRANSLATE.setValue( qoEnums.tr("Transcribe + Translate Audio/Video") ) + BatchTaskTypeEnum.SOFT.setValue( qoEnums.tr("Create Soft Subtitle Video") ) + BatchTaskTypeEnum.HARD.setValue( 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") - SubtitleLayoutEnum.TRANSLATE_ON_TOP._value_ = qoEnums.tr("Translated on Top") + SubtitleLayoutEnum.ONLY_ORIGINAL.setValue( qoEnums.tr("Original Only") ) + SubtitleLayoutEnum.ONLY_TRANSLATE.setValue( qoEnums.tr("Translated Only") ) + SubtitleLayoutEnum.ORIGINAL_ON_TOP.setValue( qoEnums.tr("Original on Top") ) + SubtitleLayoutEnum.TRANSLATE_ON_TOP.setValue( qoEnums.tr("Translated on Top")) - InternetTranslateEnum.GOOGLE._value_ = qoEnums.tr("Google Translate") + InternetTranslateEnum.GOOGLE.setValue( qoEnums.tr("Google Translate")) - TodoWhenDoneEnum.NOTHING._value_ = qoEnums.tr("Nothing") - TodoWhenDoneEnum.EXIT._value_ = qoEnums.tr("Exit The Program") - TodoWhenDoneEnum.SHUTDOWN._value_ = qoEnums.tr("Shutdown The Computer") - TodoWhenDoneEnum.SUSPEND._value_ = qoEnums.tr("Suspend The Computer") + TodoWhenDoneEnum.NOTHING.setValue( qoEnums.tr("Nothing")) + TodoWhenDoneEnum.EXIT.setValue( qoEnums.tr("Exit The Program")) + TodoWhenDoneEnum.SHUTDOWN.setValue( qoEnums.tr("Shutdown The Computer")) + TodoWhenDoneEnum.SUSPEND.setValue( qoEnums.tr("Suspend The Computer")) - Task.Status.CANCELED._value_ = qoEnums.tr("Canceled") - Task.Status.COMPLETED._value_ = qoEnums.tr("Completed") - Task.Status.DOWNLOADING._value_ = qoEnums.tr("Downloading") - Task.Status.FAILED._value_ = qoEnums.tr("Failed") - Task.Status.GENERATING._value_ = qoEnums.tr("Generating") - Task.Status.OPTIMIZING._value_ = qoEnums.tr("Optimizing") - Task.Status.PENDING._value_ = qoEnums.tr("Pending") - Task.Status.SYNTHESIZING._value_ = qoEnums.tr("Synthesizing") - Task.Status.TRANSCODING._value_ = qoEnums.tr("Transcoding") - Task.Status.TRANSLATING._value_ = qoEnums.tr("Translating") - Task.Status.WAITINGAUDIO._value_ = qoEnums.tr("Waiting for audio transcoding") - Task.Status.WAITINGOPTIMIZE._value_ = qoEnums.tr("Waiting for optimization") - Task.Status.WAITINGSYNTHESIS._value_ = qoEnums.tr("Waiting for video synthesis") - Task.Status.WAITINGTRANSCRIBE._value_ = qoEnums.tr("Waiting for transcripting") - - Task.Source.FILE_IMPORT._value_ = qoEnums.tr("File Import") - Task.Source.URL_IMPORT._value_ = qoEnums.tr("URL Import") + Task.Status.CANCELED.setValue( qoEnums.tr("Canceled")) + Task.Status.COMPLETED.setValue( qoEnums.tr("Completed")) + Task.Status.DOWNLOADING.setValue( qoEnums.tr("Downloading")) + Task.Status.FAILED.setValue( qoEnums.tr("Failed")) + Task.Status.GENERATING.setValue( qoEnums.tr("Generating")) + Task.Status.OPTIMIZING.setValue( qoEnums.tr("Optimizing")) + Task.Status.PENDING.setValue( qoEnums.tr("Pending")) + Task.Status.SYNTHESIZING.setValue( qoEnums.tr("Synthesizing")) + Task.Status.TRANSCODING.setValue( qoEnums.tr("Transcoding")) + Task.Status.TRANSLATING.setValue( qoEnums.tr("Translating")) + Task.Status.WAITINGAUDIO.setValue( qoEnums.tr("Waiting for audio transcoding")) + Task.Status.WAITINGOPTIMIZE.setValue( qoEnums.tr("Waiting for optimization")) + Task.Status.WAITINGSYNTHESIS.setValue( qoEnums.tr("Waiting for video synthesis")) + Task.Status.WAITINGTRANSCRIBE.setValue( qoEnums.tr("Waiting for transcripting")) + + Task.Source.FILE_IMPORT.setValue( qoEnums.tr("File Import")) + Task.Source.URL_IMPORT.setValue( qoEnums.tr("URL Import")) - 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 + Task.Type.OPTIMIZE.setValue( qoEnums.tr("Optimize + Translate Subtitles")) + Task.Type.SUBTITLE.setValue( qoEnums.tr("Add Subtitle To Video")) + Task.Type.SYNTHESIS.setValue( qoEnums.tr("Combine Subtitle with Video")) + Task.Type.TRANSCRIBE.setValue( qoEnums.tr("Get Subtitle From Video/Audio")) + Task.Type.URL.setValue( 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 c39cab4..04e7868 100644 --- a/app/common/signal_bus.py +++ b/app/common/signal_bus.py @@ -18,6 +18,9 @@ class SignalBus(QObject): need_video_changed = pyqtSignal(bool) soft_subtitle_changed = pyqtSignal(bool) + # App log signal + app_log_signal = pyqtSignal(str) + # 新增视频控制相关信号 video_play = pyqtSignal() # 播放信号 video_pause = pyqtSignal() # 暂停信号 diff --git a/app/components/EnumComboBoxSettingCard.py b/app/components/EnumComboBoxSettingCard.py index 37abb01..379d7a6 100644 --- a/app/components/EnumComboBoxSettingCard.py +++ b/app/components/EnumComboBoxSettingCard.py @@ -4,8 +4,7 @@ from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtGui import QIcon from qfluentwidgets import SettingCard, ComboBox -from qfluentwidgets.common.config import ConfigItem, qconfig , ConfigValidator, ConfigSerializer - +from qfluentwidgets.common.config import ConfigItem, qconfig class EnumComboBoxSettingCard(SettingCard): """ 针对多语言的Enum选项设计的下拉框设置卡片 """ @@ -27,15 +26,16 @@ def __init__(self, configItem: ConfigItem, icon: Union[str, QIcon], title: str, self.comboBox.addItem( item.value ) # 设置布局 - self.hBoxLayout.addWidget(self.comboBox, 0, Qt.AlignRight) + self.hBoxLayout.addWidget(self.comboBox, 0, Qt.AlignmentFlag.AlignRight) self.hBoxLayout.addSpacing(16) # 设置最小宽度 self.comboBox.setMinimumWidth(100) # 设置初始值 - name = qconfig.get(configItem) # It's the key, not the value - self.setValue(enums[name].value) # Set the text to translated value + itemEnum = qconfig.get(configItem) # It's the enum, not the name or value + self.comboBox.setText(itemEnum.value) + # self.setValue(itemEnum.value) # Set the text to translated value # 连接信号 self.comboBox.currentTextChanged.connect(self.__onTextChanged) @@ -48,56 +48,20 @@ def __onTextChanged(self, text: str): def setValue(self, value: str): """ 设置值 """ - key = self.getKey(value) # Get the name from Enum by value - qconfig.set(self.configItem, key) + enum = self.enums(value) # Get the enum from value + qconfig.set(self.configItem, enum) self.comboBox.setText(value) - def addItems(self, items: List[str]): + + def addItems(self, items: List[Enum]): """ 添加选项 """ + # Here the items should be enums, not str for item in items: - self.comboBox.addItem(item) + self.comboBox.addItem(item.value) - def setItems(self, items: List[str]): + def setItems(self, items: List[Enum]): """ 重新设置选项列表 """ self.comboBox.clear() - self.items = items + # self.items = items for item in items: - self.comboBox.addItem(item) - - def getKey(self, value) -> str: - # Get the key str from value str - for item in self.enums: - if item.value == value: - return item.name - return "" - - -class EnumOptionsValidator(ConfigValidator): - """ Enum Options validator """ - - def __init__(self, enums: Enum): - - if type(enums) == type(Enum): - self.names = list(item.name for item in enums) - else: - self.names = [] - - def validate(self, value): - return value in self.names - - def correct(self, value): - return value if self.validate(value) else self.names[0] - -class EnumExSerializer(ConfigSerializer): - """ enumeration class serializer for multi-language """ - - def __init__(self, enumClass): - self.enumClass = enumClass - - def serialize(self, key): - # From configItem.value to text - return key - - def deserialize(self, name): - # From text to configItem.value - return name \ No newline at end of file + self.comboBox.addItem(item.value) diff --git a/app/core/bk_asr/ASRData.py b/app/core/bk_asr/ASRData.py index 8388e87..a56e94c 100644 --- a/app/core/bk_asr/ASRData.py +++ b/app/core/bk_asr/ASRData.py @@ -3,7 +3,7 @@ from typing import List, Tuple from pathlib import Path import math -from ...common.config import SubtitleLayoutEnum as SubEnum +from ...core.entities import SubtitleLayoutEnum as SubEnum class ASRDataSeg: def __init__(self, text: str, start_time: int, end_time: int): @@ -139,7 +139,7 @@ def split_to_word_segments(self) -> 'ASRData': self.segments = new_segments - def save(self, save_path: str, ass_style: str = None, layout: str = SubEnum.ONLY_TRANSLATE.name) -> None: + def save(self, save_path: str, ass_style: str = None, layout: SubEnum = SubEnum.ONLY_ORIGINAL) -> None: """Save the ASRData to a file""" Path(save_path).parent.mkdir(parents=True, exist_ok=True) # Cannot use match/case here. Too much extra calculations. @@ -155,7 +155,7 @@ def save(self, save_path: str, ass_style: str = None, layout: str = SubEnum.ONLY else: raise ValueError(f"Unsupported file extension: {save_path}") - def to_txt(self, save_path=None, layout: str = SubEnum.ONLY_TRANSLATE.name) -> str: + def to_txt(self, save_path=None, layout: SubEnum = SubEnum.ONLY_TRANSLATE) -> str: """Convert to plain text subtitle format (without timestamps)""" result = [] for seg in self.segments: @@ -167,13 +167,13 @@ def to_txt(self, save_path=None, layout: str = SubEnum.ONLY_TRANSLATE.name) -> s # 根据字幕类型组织文本 match layout: - case SubEnum.ORIGINAL_ON_TOP.name: + case SubEnum.ORIGINAL_ON_TOP: text = f"{original}\n{translated}" if translated else original - case SubEnum.TRANSLATE_ON_TOP.name: + case SubEnum.TRANSLATE_ON_TOP: text = f"{translated}\n{original}" if translated else original - case SubEnum.ONLY_ORIGINAL.name: + case SubEnum.ONLY_ORIGINAL: text = original - case SubEnum.ONLY_TRANSLATE.name: + case SubEnum.ONLY_TRANSLATE: text = translated if translated else original case _: text = seg.transcript @@ -185,7 +185,7 @@ def to_txt(self, save_path=None, layout: str = SubEnum.ONLY_TRANSLATE.name) -> s f.write("\n".join(result)) return text - def to_srt(self, layout: str = SubEnum.ONLY_TRANSLATE.name, save_path=None) -> str: + def to_srt(self, layout: SubEnum = SubEnum.ONLY_TRANSLATE, save_path=None) -> str: """Convert to SRT subtitle format""" srt_lines = [] for n, seg in enumerate(self.segments, 1): @@ -197,13 +197,13 @@ def to_srt(self, layout: str = SubEnum.ONLY_TRANSLATE.name, save_path=None) -> s # 根据字幕类型组织文本 match layout: - case SubEnum.ORIGINAL_ON_TOP.name: + case SubEnum.ORIGINAL_ON_TOP: text = f"{original}\n{translated}" if translated else original - case SubEnum.TRANSLATE_ON_TOP.name: + case SubEnum.TRANSLATE_ON_TOP: text = f"{translated}\n{original}" if translated else original - case SubEnum.ONLY_ORIGINAL.name: + case SubEnum.ONLY_ORIGINAL: text = original - case SubEnum.ONLY_TRANSLATE.name: + case SubEnum.ONLY_TRANSLATE: text = translated if translated else original case _: text = seg.transcript @@ -242,7 +242,7 @@ def to_json(self) -> dict: } return result_json - def to_ass(self, style_str: str = None, layout: str = SubEnum.ONLY_TRANSLATE.name, save_path: str = None) -> str: + def to_ass(self, style_str: str = None, layout: SubEnum = SubEnum.ONLY_TRANSLATE, save_path: str = None) -> str: """转换为ASS字幕格式 Args: @@ -282,20 +282,22 @@ def to_ass(self, style_str: str = None, layout: str = SubEnum.ONLY_TRANSLATE.nam start_time, end_time = seg.to_ass_ts() if "\n" in seg.text: original, translate = seg.text.split("\n", 1) + else: + original = seg.text - match layout: - case SubEnum.ORIGINAL_ON_TOP.name if translate: - ass_content += dialogue_template.format(start_time, end_time, "Secondary", translate) - ass_content += dialogue_template.format(start_time, end_time, "Default", original) - case SubEnum.TRANSLATE_ON_TOP.name if translate: - ass_content += dialogue_template.format(start_time, end_time, "Secondary", original) - ass_content += dialogue_template.format(start_time, end_time, "Default", translate) - case SubEnum.ONLY_ORIGINAL.name: - ass_content += dialogue_template.format(start_time, end_time, "Default", original) - case SubEnum.ONLY_TRANSLATE.name if translate: - ass_content += dialogue_template.format(start_time, end_time, "Default", translate) - case _: - ass_content += dialogue_template.format(start_time, end_time, "Default", seg.text) + match layout: + case SubEnum.ORIGINAL_ON_TOP if translate: + ass_content += dialogue_template.format(start_time, end_time, "Secondary", translate) + ass_content += dialogue_template.format(start_time, end_time, "Default", original) + case SubEnum.TRANSLATE_ON_TOP if translate: + ass_content += dialogue_template.format(start_time, end_time, "Secondary", original) + ass_content += dialogue_template.format(start_time, end_time, "Default", translate) + case SubEnum.ONLY_ORIGINAL: + ass_content += dialogue_template.format(start_time, end_time, "Default", original) + case SubEnum.ONLY_TRANSLATE if translate: + ass_content += dialogue_template.format(start_time, end_time, "Default", translate) + case _: + ass_content += dialogue_template.format(start_time, end_time, "Default", original) if save_path: Path(save_path).parent.mkdir(parents=True, exist_ok=True) @@ -361,7 +363,6 @@ def __str__(self): def add_minimum_len(self, min_len_ms=1500): # Set each sentence's minimum time length. Default is 1.5 seconds. # This method doesn't work with word segments, but it will be applied anyway - for i in range(len(self.segments)-1): seg = self.segments[i] if seg.end_time - seg.start_time < min_len_ms: diff --git a/app/core/entities.py b/app/core/entities.py index b4c2def..0839b42 100644 --- a/app/core/entities.py +++ b/app/core/entities.py @@ -4,24 +4,44 @@ from enum import Enum from random import randint from typing import Optional -from PyQt5.QtCore import QObject -class BatchTaskTypeEnum(Enum): + +class MuEnum(Enum): + """ Mutable Enum. Unlike regular Enum, its values can be set again. """ + def setValue(self, newValue): + # So far it works fine. Maybe it won't work in the furture. + # Use on members like myEnum.member.setValue(newValue) + oldValue = self.value + self._value_ = newValue + v2m_dict = self.__class__.__dict__['_value2member_map_'] + v2m_dict.pop( oldValue, None ) + v2m_dict[newValue] = self + +class BatchTaskTypeEnum(MuEnum): """ 批量任务类型 """ TRANSCRIBE = "Create Transcription from Audio/Video" TRANSLATE = "Transcribe + Translate Audio/Video" SOFT = "Create Soft Subtitle Video" HARD = "Create Hard Subtitle Video" -class SubtitleLayoutEnum(Enum): +class SubtitleLayoutEnum(MuEnum): """ 字幕布局 """ ONLY_ORIGINAL = "Original Only" ONLY_TRANSLATE = "Translated Only" ORIGINAL_ON_TOP = "Original On Top" TRANSLATE_ON_TOP = "Translated On Top" - - -class InternetTranslateEnum(Enum): + @classmethod + def add_member(cls, name, value): + if name not in cls.__members__: + member = object.__new__(cls) + member._value_ = value + member._name_ = name + cls.__members__[name] = member + else: + raise ValueError(f"Member '{name}' already exists") + + +class InternetTranslateEnum(MuEnum): """网络翻译""" GOOGLE = "Google Translate" @@ -223,7 +243,7 @@ class TargetLanguageEnum(Enum): CANTONESE = "Cantonese" -class TodoWhenDoneEnum(Enum): +class TodoWhenDoneEnum(MuEnum): """ 批量处理完成后需做事情 """ NOTHING = "Nothing" SUSPEND = "Suspend the computer" @@ -485,7 +505,7 @@ class FasterWhisperModelEnum(Enum): @dataclass class Task: - class Status(Enum): + class Status(MuEnum): """ 任务状态 (下载、转录、优化、翻译、生成) """ PENDING = "Pending" DOWNLOADING = "Downloading" @@ -503,11 +523,11 @@ class Status(Enum): FAILED = "Failed" CANCELED = "Canceled" - class Source(Enum): + class Source(MuEnum): FILE_IMPORT = "File Import" URL_IMPORT = "URL Import" - class Type(Enum): + class Type(MuEnum): # 任务类型:transcribe or generate subtitle TRANSCRIBE = "Get Subtitle From Video/Audio" TRANSLATE = "Transcribe Video/Audio then Translate" @@ -539,7 +559,7 @@ class Type(Enum): audio_save_path: Optional[str] = None # 转录(转录模型) - transcribe_model: Optional[TranscribeModelEnum] = TranscribeModelEnum.JIANYING.value + 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 diff --git a/app/core/thread/create_task_thread.py b/app/core/thread/create_task_thread.py index 88296ae..e4e5200 100644 --- a/app/core/thread/create_task_thread.py +++ b/app/core/thread/create_task_thread.py @@ -21,7 +21,7 @@ class CreateTaskThread(QThread): progress = pyqtSignal(int, str) error = pyqtSignal(str) - def __init__(self, file_path, task_type: Task.Type, soft_sub: bool): + def __init__(self, file_path, task_type: Task.Type, soft_sub: bool = True): super().__init__() self.file_path = file_path self.task_type = task_type @@ -31,14 +31,14 @@ def run(self): try: match self.task_type: case Task.Type.SUBTITLE: - self.create_file_task(self.file_path, self.soft_sub, need_video = True) + self.create_file_task(self.file_path, self.task_type, self.soft_sub, need_video = True) case Task.Type.URL: # Here whether to do the final video synthesis depends on config value - self.create_url_task(self.file_path, self.soft_sub, cfg.need_video.value) + self.create_url_task(self.file_path, self.task_type, self.soft_sub, cfg.need_video.value) case Task.Type.TRANSCRIBE: - self.create_transcription_task(self.file_path) + self.create_transcription_task(self.file_path, self.task_type) case Task.Type.TRANSLATE: - self.create_file_task(self.file_path, self.soft_sub, need_video = False) + self.create_file_task(self.file_path, self.task_type, self.soft_sub, need_video = False) case _: ValueError("No matching task type.") except Exception as e: @@ -46,7 +46,7 @@ def run(self): self.progress.emit(0, self.tr("创建任务失败")) self.error.emit(str(e)) - def create_file_task(self, file_path, soft_sub: bool, need_video: bool): + def create_file_task(self, file_path, task_type: Task.Type, soft_sub: bool, need_video: bool): logger.info("\n===================") logger.info(f"开始创建文件任务:{file_path}") # 使用 Path 对象处理路径 @@ -60,7 +60,7 @@ def create_file_task(self, file_path, soft_sub: bool, need_video: bool): video_info = get_video_info(file_path, thumbnail_path=thumbnail_path) video_info = VideoInfo(**video_info) - match cfg.transcribe_model.value: + match cfg.transcribe_model.value.value: case TranscribeModelEnum.WHISPER.value: whisper_type = f"-{cfg.whisper_model.value.value}-{cfg.transcribe_language.value.value}" case TranscribeModelEnum.WHISPER_API.value: @@ -89,8 +89,26 @@ def create_file_task(self, file_path, soft_sub: bool, need_video: bool): else: subtitle_style_srt = None - need_word_time_stamp = cfg.transcribe_model.value in [TranscribeModelEnum.JIANYING.value, TranscribeModelEnum.BIJIAN.value] - + need_word_time_stamp = cfg.transcribe_model.value.value in [TranscribeModelEnum.JIANYING.value, TranscribeModelEnum.BIJIAN.value] + + match task_type: + case Task.Type.OPTIMIZE: + need_optimze = True + need_translate = True + case Task.Type.TRANSLATE: + need_optimze = False + need_translate = True + case Task.Type.TRANSCRIBE: + need_optimze = False + need_translate = False + case Task.Type.SUBTITLE: + need_optimze = cfg.need_optimize.value + need_translate = cfg.need_translate.value + case Task.Type.URL: + need_optimze = cfg.need_optimize.value + need_translate = cfg.need_translate.value + + # 创建 Task 对象 task = Task( id=0, @@ -132,8 +150,8 @@ def create_file_task(self, file_path, soft_sub: bool, need_video: bool): base_url=cfg.api_base.value, api_key=cfg.api_key.value, llm_model=cfg.model.value, - need_translate=cfg.need_translate.value, - need_optimize=cfg.need_optimize.value, + need_translate=need_translate, + need_optimize=need_optimze, max_word_count_cjk=cfg.max_word_count_cjk.value, max_word_count_english=cfg.max_word_count_english.value, need_split=cfg.need_split.value, @@ -144,13 +162,13 @@ def create_file_task(self, file_path, soft_sub: bool, need_video: bool): subtitle_style_srt=subtitle_style_srt, need_video=need_video, vertical_offset=cfg.vertical_offset.value, - type=Task.Type.SUBTITLE, + type=task_type, ) self.finished.emit(task) self.progress.emit(100, self.tr("创建任务完成")) logger.info(f"文件任务创建完成:{task}") - def create_url_task(self, url, soft_sub: bool, need_video: bool): + def create_url_task(self, task_type: Task.Type, url, soft_sub: bool, need_video: bool): logger.info("\n===================") logger.info(f"开始创建URL任务:{url}") self.progress.emit(5, self.tr("正在获取视频信息")) @@ -169,7 +187,8 @@ def create_url_task(self, url, soft_sub: bool, need_video: bool): video_codec=info_dict.get('vcodec', ''), audio_codec=info_dict.get('acodec', ''), audio_sampling_rate=info_dict.get('asr', 0), - thumbnail_path=thumbnail_file_path + thumbnail_path=thumbnail_file_path, + type = task_type ) # 使用 Path 对象处理路径 @@ -177,7 +196,7 @@ def create_url_task(self, url, soft_sub: bool, need_video: bool): task_work_dir = file_full_path.parent file_name = file_full_path.stem - match cfg.transcribe_model.value: + match cfg.transcribe_model.value.value: case TranscribeModelEnum.WHISPER.value: whisper_type = f"{cfg.whisper_model.value.value}-{cfg.transcribe_language.value.value}" case TranscribeModelEnum.WHISPER_API.value: @@ -189,16 +208,16 @@ def create_url_task(self, url, soft_sub: bool, need_video: bool): # 定义各个路径 audio_save_path = task_work_dir / f"{self.tr("【音频】")}{Path(video_file_path).stem}.wav" - original_subtitle_save_path = task_work_dir / f"{self.tr("【原始字幕】")}{cfg.transcribe_model.value}-file_name-{whisper_type}.srt" if not subtitle_file_path else subtitle_file_path + original_subtitle_save_path = task_work_dir / f"{self.tr("【原始字幕】")}{cfg.transcribe_model.value.value}-file_name-{whisper_type}.srt" if not subtitle_file_path else subtitle_file_path result_subtitle_save_path = task_work_dir / ( cfg.subtitle_file_prefix.value + file_name + cfg.subtitle_file_suffix.value + "." + cfg.subtitle_output_format.value.value ) video_save_path = task_work_dir / f"{self.tr("【生成】")}{Path(video_file_path).name}" # 音频处理 audio_format = "pcm_s16le" # for all other audio format - if video_info.audio_codec in ["mp3", "pcm"]: + if video_info.audio_codec in [ "mp3", "pcm"]: audio_format = "copy" - if cfg.transcribe_model.value in [TranscribeModelEnum.JIANYING.value, TranscribeModelEnum.BIJIAN.value]: + if cfg.transcribe_model.value.value in [TranscribeModelEnum.JIANYING.value, TranscribeModelEnum.BIJIAN.value]: need_word_time_stamp = True else: need_word_time_stamp = False @@ -269,8 +288,9 @@ def create_url_task(self, url, soft_sub: bool, need_video: bool): self.finished.emit(task) logger.info(f"URL任务创建完成:{task}") - def create_transcription_task(self, file_path): + def create_transcription_task(self, file_path, task_type: Task.Type): logger.info(f"开始创建转录任务:{file_path}") + # task_work_dir = Path(file_path).parent # 使用 Path 对象处理路径 @@ -283,7 +303,7 @@ def create_transcription_task(self, file_path): video_info = VideoInfo(**video_info) # 定义各个路径 - match cfg.transcribe_model.value: + match cfg.transcribe_model.value.value: case TranscribeModelEnum.WHISPER.value: whisper_type = f"{cfg.whisper_model.value.value}-{cfg.transcribe_language.value.value}" case TranscribeModelEnum.WHISPER_API.value: @@ -296,11 +316,11 @@ def create_transcription_task(self, file_path): # 音频处理 audio_save_path = task_work_dir / f"Audio_{file_name}.wav" audio_format = "pcm_s16le" # for all other audio format - if video_info.audio_codec in ["mp3", "pcm"]: + if video_info.audio_codec in [ "mp3", "pcm"]: audio_format = "copy" audio_save_path = task_work_dir / f"Audio_{file_name}.wav" - original_subtitle_save_path = task_work_dir / f"【原始字幕】{file_name}-{cfg.transcribe_model.value}-{whisper_type}.srt" + 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": @@ -310,7 +330,7 @@ def create_transcription_task(self, file_path): subtitle_style_srt = ass_style_path.read_text(encoding="utf-8") else: subtitle_style_srt = None - + # 创建 Task 对象 task = Task( id=0, @@ -323,8 +343,11 @@ def create_transcription_task(self, file_path): file_path=str(Path(self.file_path)), url="", source=Task.Source.FILE_IMPORT, + base_url=cfg.api_base.value, + api_key=cfg.api_key.value, + llm_model=cfg.model.value, original_language=cfg.transcribe_language.value, - target_language=cfg.target_language.value.value, + target_language=cfg.transcribe_language.value, transcribe_language=LANGUAGES[cfg.transcribe_language.value.value], whisper_model=cfg.whisper_model.value.value, whisper_api_key=cfg.whisper_api_key.value, @@ -353,23 +376,29 @@ def create_transcription_task(self, file_path): subtitle_layout=cfg.subtitle_layout.value, max_word_count_cjk=cfg.max_word_count_cjk.value, max_word_count_english=cfg.max_word_count_english.value, - type=Task.Type.TRANSCRIBE, # Transcribe only, no video generation. + need_video=False, + soft_subtitle=True, + need_optimize=False, + need_translate=False, + type=task_type, # It should be Transcribe only, no video generation. # Don't set need_video here because it can be part of subtitle pipeline ) self.finished.emit(task) logger.info(f"转录任务创建完成:{task}") - def create_subtitle_optimization_task(file_path): - logger.info(f"开始创建字幕优化任务:{file_path}") + def create_subtitle_optimization_task(file_path, task_type: Task.Type): + # This includes optimize+ translate and single translate + logger.info(f"开始创建字幕优化翻译任务:{file_path}") file_full_path = Path(file_path) task_work_dir = Path(file_path).parent + + need_optimze = True if task_type == Task.Type.OPTIMIZE else False - if cfg.need_translate.value: - result_subtitle_type = qoCreateTask.tr("【翻译字幕】") - elif cfg.need_optimize.value: - result_subtitle_type = qoCreateTask.tr("【修正字幕】") + if need_optimze: + result_subtitle_type = qoCreateTask.tr("【反思+翻译字幕】") else: - result_subtitle_type = qoCreateTask.tr("【字幕】") + result_subtitle_type = qoCreateTask.tr("【单句翻译字幕】") + logger.info(f"字幕类型: {result_subtitle_type}") original_subtitle_save_path = task_work_dir / file_path @@ -395,8 +424,8 @@ def create_subtitle_optimization_task(file_path): base_url=cfg.api_base.value, api_key=cfg.api_key.value, llm_model=cfg.model.value, - need_translate=cfg.need_translate.value, - need_optimize=cfg.need_optimize.value, + need_translate=True, + need_optimize=need_optimze, result_subtitle_save_path=str(result_subtitle_save_path), thread_num=cfg.thread_num.value, batch_size=cfg.batch_size.value, @@ -405,7 +434,7 @@ def create_subtitle_optimization_task(file_path): max_word_count_cjk=cfg.max_word_count_cjk.value, max_word_count_english=cfg.max_word_count_english.value, subtitle_style_srt=subtitle_style_srt, - type=Task.Type.OPTIMIZE, + type=task_type, # Don't set need_video here because it can be part of subtitle pipeline ) logger.info(f"字幕优化任务创建完成:{task}") diff --git a/app/core/thread/subtitle_optimization_thread.py b/app/core/thread/subtitle_optimization_thread.py index 9c52261..f6bd48b 100644 --- a/app/core/thread/subtitle_optimization_thread.py +++ b/app/core/thread/subtitle_optimization_thread.py @@ -56,24 +56,11 @@ def set_custom_prompt_text(self, text: str): def _setup_api_config(self): """设置API配置,返回base_url, api_key, llm_model, thread_num, batch_size""" - if self.task.base_url: - if not test_openai(self.task.base_url, self.task.api_key, self.task.llm_model)[0]: - raise Exception(self.tr("OpenAI API 测试失败, 请检查设置")) - return (self.task.base_url, self.task.api_key, self.task.llm_model, - self.task.thread_num, self.task.batch_size) + if not test_openai(self.task.base_url, self.task.api_key, self.task.llm_model)[0]: + raise Exception(self.tr("OpenAI API 测试失败, 请检查设置")) + return (self.task.base_url, self.task.api_key, self.task.llm_model, + self.task.thread_num, self.task.batch_size) - logger.info("尝试使用自带的API配置") - # 遍历配置字典找到第一个可用的API - for config in FREE_API_CONFIGS.values(): - if not self.valid_limit(): - raise Exception(self.tr("公益服务有限!请配置自己的API!")) - if test_openai(config["base_url"], config["api_key"], config["llm_model"])[0]: - self.set_limit() - return (config["base_url"], config["api_key"], config["llm_model"], - config["thread_num"], config["batch_size"]) - - logger.error("自带的API配置暂时不可用,请配置自己的API") - raise Exception(self.tr("自带的API配置暂时不可用,请配置自己的大模型API")) def run(self): doingOptimizing = False @@ -93,7 +80,7 @@ def run(self): target_language = self.task.target_language need_translate = self.task.need_translate need_optimize = self.task.need_optimize - need_summarize = True + need_summarize = True if need_optimize else False subtitle_style_srt = self.task.subtitle_style_srt subtitle_layout = self.task.subtitle_layout max_word_count_cjk = self.task.max_word_count_cjk @@ -128,11 +115,10 @@ def run(self): asr_data = from_subtitle_file(str_path) + """ # 检查是否需要合并重新断句 - 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 is_word_split: + # Right now this will be done right after transcribing. + if asr_data.is_word_timestamp(): self.progress.emit(15, self.tr("字幕断句...")) logger.info("正在字幕断句...") asr_data = merge_segments(asr_data, model=llm_model, @@ -141,8 +127,7 @@ def run(self): max_word_count_english=max_word_count_english) 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()} @@ -229,27 +214,6 @@ def run(self): if doingOptimizing: cfg.gbDoingOptimizing = False - def set_limit(self): - self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, - 'VideoCaptioner', 'VideoCaptioner') - current_date = time.strftime('%Y-%m-%d') - last_date = self.settings.value('llm/last_date', '') - if current_date != last_date: - self.settings.setValue('llm/last_date', current_date) - self.settings.setValue('llm/daily_calls', 0) - self.settings.sync() # 强制写入 - - def valid_limit(self): - self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, - 'VideoCaptioner', 'VideoCaptioner') - daily_calls = int(self.settings.value('llm/daily_calls', 0)) - if daily_calls >= self.MAX_DAILY_LLM_CALLS: - return False - self.settings.setValue('llm/daily_calls', daily_calls + 1) - self.settings.sync() # 强制写入 - print(self.settings.value('llm/daily_calls', 0)) - return True - def callback(self, result: Dict): self.finished_subtitle_length += len(result) progress_num = int((self.finished_subtitle_length / self.subtitle_length) * 70) + 30 diff --git a/app/core/thread/transcript_thread.py b/app/core/thread/transcript_thread.py index c30d064..096055d 100644 --- a/app/core/thread/transcript_thread.py +++ b/app/core/thread/transcript_thread.py @@ -1,7 +1,9 @@ -import datetime, time +import time, os +import logging from pathlib import Path -from PyQt5.QtCore import QThread, pyqtSignal +from PyQt5.QtCore import QThread, pyqtSignal, QSettings +from ...common.signal_bus import signalBus from ..bk_asr import ( JianYingASR, @@ -11,11 +13,15 @@ WhisperAPI, FasterWhisperASR ) -from ..entities import Task, TranscribeModelEnum +from ..bk_asr.ASRData import ASRData +from ..subtitle_processor.spliter import merge_segments +from ..entities import Task, TranscribeModelEnum, SubtitleLayoutEnum from ..utils.video_utils import video2audio from ..utils.logger import setup_logger +from ..utils.test_opanai import test_openai from ...config import MODEL_PATH from ...common.config import cfg +from ...core.thread.subtitle_optimization_thread import FREE_API_CONFIGS logger = setup_logger("transcript_thread") @@ -96,10 +102,10 @@ def run(self): doingTranscribe = True # 获取ASR模型 - asr_class = self.ASR_MODELS.get(self.task.transcribe_model) + asr_class = self.ASR_MODELS.get(self.task.transcribe_model) # Use the Enum instead of Enum.value if not asr_class: - logger.error("无效的转录模型: %s", str(self.task.transcribe_model)) - raise ValueError(self.tr("无效的转录模型: ") + str(self.task.transcribe_model)) # 检查转录模型是否有效 + logger.error("无效的转录模型: %s", str(self.task.transcribe_model.value)) + raise ValueError(self.tr("无效的转录模型: ") + str(self.task.transcribe_model.value)) # 检查转录模型是否有效 # 执行转录 args = { @@ -107,13 +113,13 @@ def run(self): "need_word_time_stamp": self.task.need_word_time_stamp, } match self.task.transcribe_model: - case TranscribeModelEnum.WHISPER.value: + case TranscribeModelEnum.WHISPER: args["language"] = self.task.transcribe_language args["whisper_model"] = self.task.whisper_model args["use_cache"] = False args["need_word_time_stamp"] = True self.asr = WhisperASR(self.task.audio_save_path, **args) - case TranscribeModelEnum.WHISPER_API.value: + case TranscribeModelEnum.WHISPER_API: args["language"] = self.task.transcribe_language args["whisper_model"] = self.task.whisper_api_model args["api_key"] = self.task.whisper_api_key @@ -122,7 +128,7 @@ def run(self): args["use_cache"] = False args["need_word_time_stamp"] = True self.asr = WhisperAPI(self.task.audio_save_path, **args) - case TranscribeModelEnum.FASTER_WHISPER.value: + case TranscribeModelEnum.FASTER_WHISPER: args["faster_whisper_path"] = cfg.faster_whisper_program.value args["whisper_model"] = self.task.faster_whisper_model.value args["model_dir"] = str(MODEL_PATH) @@ -153,15 +159,22 @@ def run(self): args["repetition_penalty"] = self.task.faster_whisper_repetion_penalty self.asr = FasterWhisperASR(self.task.audio_save_path, **args) - case TranscribeModelEnum.BIJIAN.value: + case TranscribeModelEnum.BIJIAN: self.asr = BcutASR(self.task.audio_save_path, **args) - case TranscribeModelEnum.JIANYING.value: + case TranscribeModelEnum.JIANYING: self.asr = JianYingASR(self.task.audio_save_path, **args) case _: - raise ValueError(self.tr("无效的转录模型: ") + str(self.task.transcribe_model)) + raise ValueError(self.tr("无效的转录模型: ") + str(self.task.transcribe_model.value)) asr_data = self.asr.run(callback=self.progress_callback) + if asr_data.is_word_timestamp(): + # The data is in words + asr_data = self.merge_words(asr_data) + if not asr_data: + # word merging failed + raise ValueError(self.tr("智能断句失败,请检查你的大模型Base URL和API Key是否有效。")) + # Check if asr_data needs to add minimum length if cfg.subtitle_enable_sentence_minimum_time.value: asr_data.add_minimum_len(cfg.subtitle_sentence_minimum_time.value) @@ -183,7 +196,7 @@ def run(self): asr_data.save( save_path=self.task.result_subtitle_save_path, ass_style=self.task.subtitle_style_srt, - layout=self.task.subtitle_layout + layout=SubtitleLayoutEnum.ONLY_ORIGINAL, ) logger.info("目的字幕文件已保存到: %s", self.task.result_subtitle_save_path) @@ -212,8 +225,43 @@ def run(self): def progress_callback(self, value, message): progress = min(20 + (value * 0.8), 100) self.progress.emit(int(progress), message) - + + def _setup_api_config(self): + """设置API配置,返回base_url, api_key, llm_model, thread_num, batch_size""" + print(f"base: {self.task.base_url} key:{self.task.api_key} model:{self.task.llm_model}") + if not test_openai(self.task.base_url, self.task.api_key, self.task.llm_model)[0]: + raise Exception(self.tr("OpenAI API 测试失败, 请检查设置")) + return (self.task.base_url, self.task.api_key, self.task.llm_model, + self.task.thread_num, self.task.batch_size) + + def merge_words(self, asr_data: ASRData) -> ASRData: + logger.info(f"\n===========字幕断句任务开始===========") + + # 获取API配置 + try: + self.progress.emit(80, self.tr("开始验证API配置...")) + base_url, api_key, llm_model, thread_num, batch_size = self._setup_api_config() + logger.info(f"使用 {llm_model} 作为LLM模型") + os.environ['OPENAI_BASE_URL'] = base_url + os.environ['OPENAI_API_KEY'] = api_key + + self.progress.emit(85, self.tr("字幕断句...")) + logger.info("正在字幕断句...") + asr_data = merge_segments(asr_data, model=llm_model, + num_threads=thread_num, + max_word_count_cjk=cfg.max_word_count_cjk.value, + max_word_count_english=cfg.max_word_count_english.value) + return asr_data + + except Exception as e: + logger.exception(f"断句失败: {str(e)}") + self.error.emit(str(e)) + self.progress.emit(100, self.tr("断句失败")) + + + + # Is the current config is using FasterWhipser and translate to English? def isFasterWhisperTranslate(self): - return cfg.transcribe_model.value == TranscribeModelEnum.FASTER_WHISPER.value and cfg.faster_whisper_translate_to_english.value + return cfg.transcribe_model.value.value == TranscribeModelEnum.FASTER_WHISPER.value and cfg.faster_whisper_translate_to_english.value diff --git a/app/core/utils/logger.py b/app/core/utils/logger.py index 76e0f37..a10a1b9 100644 --- a/app/core/utils/logger.py +++ b/app/core/utils/logger.py @@ -1,12 +1,22 @@ import logging import logging.handlers from pathlib import Path +from ...common.signal_bus import signalBus from urllib3.exceptions import InsecureRequestWarning from ...config import LOG_PATH, LOG_LEVEL +class LogHandler(logging.Handler): + def __init__(self): + super().__init__() + pass + def emit(self, record): + message = self.format(record) + signalBus.app_log_signal.emit(message) + +app_log_handler = LogHandler() def setup_logger(name: str, level: int = LOG_LEVEL, @@ -59,6 +69,10 @@ def format(self, record): file_handler.setFormatter(level_formatter) logger.addHandler(file_handler) + # Add logger for + logger.addHandler(app_log_handler) + + # 设置特定库的日志级别为ERROR以减少日志噪音 error_loggers = ["urllib3", "requests", "openai", "httpx", "httpcore", "ssl", "certifi"] for lib in error_loggers: diff --git a/app/view/batch_process_interface.py b/app/view/batch_process_interface.py index 7c0602c..497e635 100644 --- a/app/view/batch_process_interface.py +++ b/app/view/batch_process_interface.py @@ -18,7 +18,7 @@ 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, SupportedSubtitleFormats, SupportedImageFormats from ..core.entities import Task, VideoInfo, BatchTaskTypeEnum from ..core.thread.create_task_thread import CreateTaskThread @@ -91,8 +91,8 @@ def setup_ui(self): self.todo_when_done_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignCenter ) self.todo_when_done_combobox = ComboBox() self.todo_when_done_combobox.addItems(item.value for item in TodoWhenDoneEnum) - todoKey = cfg.todo_when_done.value - self.todo_when_done_combobox.setCurrentText(TodoWhenDoneEnum[todoKey].value) # Doing nothing + todoEnum = cfg.todo_when_done.value + self.todo_when_done_combobox.setCurrentText(todoEnum.value) # Doing nothing self.top_layout.addWidget(self.start_all_button) self.top_layout.addWidget(self.cancel_button) @@ -150,13 +150,7 @@ def set_default_task_type(self, whatever): self.task_type_combo.setCurrentText(BatchTaskTypeEnum.TRANSCRIBE.value) def todo_when_done_changed(self, text: str): - todoKey = None - for key in TodoWhenDoneEnum: - if key.value == text: - todoKey = key.name - break - if todoKey: - cfg.set(cfg.todo_when_done, todoKey) + cfg.set(cfg.todo_when_done, TodoWhenDoneEnum(text)) def clear_all_tasks(self): """清空所有任务""" @@ -487,22 +481,32 @@ def dropEvent(self, event): file_ext = os.path.splitext(file_path)[1][1:].lower() # 根据任务类型检查文件格式 - 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 - + match self.task_type_combo.currentText(): + case BatchTaskTypeEnum.HARD.value: + # Create hard sub video + supported_formats = {fmt.value for fmt in SupportedVideoFormats} + task_type = Task.Type.SUBTITLE + soft_sub = False + case BatchTaskTypeEnum.SOFT.value: + # Create soft sub video + supported_formats = {fmt.value for fmt in SupportedVideoFormats} + task_type = Task.Type.SUBTITLE + soft_sub = True + case BatchTaskTypeEnum.TRANSLATE.value: + # Create Optimize+Translate / Single Sentence Translate / Google Translate sub + supported_formats = {fmt.value for fmt in SupportedVideoFormats} | {fmt.value for fmt in SupportedAudioFormats} + task_type = Task.Type.TRANSLATE + soft_sub = True + case BatchTaskTypeEnum.TRANSCRIBE.value: + # Create transcrptions only + supported_formats = {fmt.value for fmt in SupportedVideoFormats} | {fmt.value for fmt in SupportedAudioFormats} + task_type = Task.Type.TRANSCRIBE + soft_sub = True + if file_ext in supported_formats: - 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 + self.create_task(file_path, task_type, soft_sub) else: - error_msg = self.tr("请拖入视频文件") if self.task_type_combo.currentText() == BatchTaskTypeEnum.TRANSCRIBE.value else self.tr("请拖入音频或视频文件") + error_msg = self.tr("请拖入视频文件") if task_type in [ BatchTaskTypeEnum.SOFT or BatchTaskTypeEnum.HARD ] else self.tr("请拖入音频或视频文件") InfoBar.error( self.tr(f"格式错误") + file_ext, error_msg, @@ -661,20 +665,20 @@ def update_tooltip(self): 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) + " " + 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 + "" + 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("字幕类型:软字幕 ") + strategy_text += self.tr(" 字幕类型:软字幕 ") else: - strategy_text += self.tr("字幕类型:硬字幕 ") + strategy_text += self.tr(" 字幕类型:硬字幕 ") if self.task.portrait: - strategy_text += self.tr("竖屏模式:开启 ") + strategy_text += self.tr(" 竖屏模式:开启 ") if self.task.portrait_background: - strategy_text += "\n" + self.tr("竖屏背景:") + 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 + "\n" + 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: diff --git a/app/view/home_interface.py b/app/view/home_interface.py index 114a324..600825b 100644 --- a/app/view/home_interface.py +++ b/app/view/home_interface.py @@ -36,7 +36,7 @@ def __init__(self, parent=None): self.video_synthesis_interface = VideoSynthesisInterface(self) self.addSubInterface(self.task_creation_interface, 'TaskCreationInterface', self.tr('任务创建')) - self.addSubInterface(self.transcription_interface, 'TranscriptionInterface', self.tr('语音转录')) + self.addSubInterface(self.transcription_interface, 'TranscriptionInterface', self.tr('语音转录/日志')) self.addSubInterface(self.subtitle_optimization_interface, 'SubtitleOptimizationInterface', self.tr('字幕优化与翻译')) self.addSubInterface(self.video_synthesis_interface, 'VideoSynthesisInterface', self.tr('字幕视频合成')) @@ -53,21 +53,22 @@ def __init__(self, parent=None): self.transcription_interface.finished.connect(self.switch_to_subtitle_optimization) self.subtitle_optimization_interface.finished.connect(self.switch_to_video_synthesis) - def switch_to_transcription(self, task): + def switch_to_transcription(self, task: Task | None): # 切换到转录界面 self.transcription_interface.set_task(task) self.transcription_interface.process() self.stackedWidget.setCurrentWidget(self.transcription_interface) self.pivot.setCurrentItem('TranscriptionInterface') - def switch_to_subtitle_optimization(self, task): + def switch_to_subtitle_optimization(self, task: Task | None): # 切换到字幕优化界面 - self.subtitle_optimization_interface.set_task(task) - self.subtitle_optimization_interface.process() - self.stackedWidget.setCurrentWidget(self.subtitle_optimization_interface) - self.pivot.setCurrentItem('SubtitleOptimizationInterface') + if task.need_optimize or task.need_translate: + self.subtitle_optimization_interface.set_task(task) + self.subtitle_optimization_interface.process() + self.stackedWidget.setCurrentWidget(self.subtitle_optimization_interface) + self.pivot.setCurrentItem('SubtitleOptimizationInterface') - def switch_to_video_synthesis(self, task): + def switch_to_video_synthesis(self, task: Task | None): # 切换到视频合成界面 self.video_synthesis_interface.set_task(task) self.video_synthesis_interface.process() diff --git a/app/view/setting_interface.py b/app/view/setting_interface.py index d7e3a22..9e4147d 100644 --- a/app/view/setting_interface.py +++ b/app/view/setting_interface.py @@ -11,17 +11,17 @@ from app.components.WhisperAPISettingDialog import WhisperAPISettingDialog from app.config import VERSION, YEAR, AUTHOR, HELP_URL, FEEDBACK_URL, RELEASE_URL -from app.core.entities import TranscribeModelEnum -from app.core.thread.version_manager_thread import VersionManager -from ..common.config import cfg, InternetTranslateEnum, SubtitleLayoutEnum +from app.core.entities import TranscribeModelEnum, SubtitleLayoutEnum, InternetTranslateEnum +from ..common.config import cfg from ..components.EditComboBoxSettingCard import EditComboBoxSettingCard -from ..components.EnumComboBoxSettingCard import * +from ..components.EnumComboBoxSettingCard import EnumComboBoxSettingCard from ..components.LineEditSettingCard import LineEditSettingCard from ..core.utils.test_opanai import test_openai, get_openai_models from ..components.WhisperSettingDialog import WhisperSettingDialog from ..components.FasterWhisperSettingDialog import FasterWhisperSettingDialog from ..common.signal_bus import signalBus + class SettingInterface(ScrollArea): """ 设置界面 """ diff --git a/app/view/subtitle_optimization_interface.py b/app/view/subtitle_optimization_interface.py index e09df7e..157d654 100644 --- a/app/view/subtitle_optimization_interface.py +++ b/app/view/subtitle_optimization_interface.py @@ -17,9 +17,9 @@ from app.config import SUBTITLE_STYLE_PATH from ..core.thread.subtitle_optimization_thread import SubtitleOptimizationThread -from ..common.config import cfg, SubtitleLayoutEnum +from ..common.config import cfg from ..core.bk_asr.ASRData import from_subtitle_file, from_json -from ..core.entities import OutputSubtitleFormatEnum, SupportedSubtitleFormats +from ..core.entities import OutputSubtitleFormatEnum, SupportedSubtitleFormats, SubtitleLayoutEnum from ..core.entities import Task from ..core.thread.create_task_thread import CreateTaskThread from ..common.signal_bus import signalBus @@ -184,8 +184,8 @@ def _setup_top_layout(self): # 添加字幕排布下拉框 self.layout_combobox = ComboBox(self) self.layout_combobox.addItems([layout.value for layout in SubtitleLayoutEnum]) - key = cfg.subtitle_layout.value - self.layout_combobox.setCurrentText(SubtitleLayoutEnum[key].value) + sub_enum = cfg.subtitle_layout.value + self.layout_combobox.setCurrentText(sub_enum.value) self.left_layout.addWidget(self.save_button) self.left_layout.addWidget(self.format_combobox) @@ -355,13 +355,12 @@ def on_subtitle_layout_changed(self, value: str): value: 新的字幕布局 """ # 更新配置中的字幕布局 - key = None - for item in SubtitleLayoutEnum: - if item.value == value: - key = item.name - if key: - cfg.subtitle_layout.value = key - # 更新下拉框的当前文本为新的布局 + enum = SubtitleLayoutEnum(value) # Get the enum from value + if enum: + if cfg.subtitle_layout.value != enum: + cfg.subtitle_layout.value = enum + # 更新下拉框的当前文本为新的布局 + cfg.save() self.layout_combobox.setCurrentText(value) def create_task(self, file_path): diff --git a/app/view/subtitle_style_interface.py b/app/view/subtitle_style_interface.py index d64c32a..b210e5e 100644 --- a/app/view/subtitle_style_interface.py +++ b/app/view/subtitle_style_interface.py @@ -11,13 +11,14 @@ PushSettingCard, FluentIcon as FIF, CardWidget, BodyLabel, ImageLabel, InfoBar, InfoBarPosition) -from ..common.config import cfg, SubtitleLayoutEnum +from ..common.config import cfg from ..components.MySettingCard import SpinBoxSettingCard, ComboBoxSettingCard, ColorSettingCard, \ DoubleSpinBoxSettingCard -from ..components.EnumComboBoxSettingCard import * +from ..components.EnumComboBoxSettingCard import EnumComboBoxSettingCard from ..core.utils.subtitle_preview import generate_preview from ..config import SUBTITLE_STYLE_PATH from ..common.signal_bus import signalBus +from ..core.entities import SubtitleLayoutEnum PERVIEW_TEXTS = { "长文本": ("This is a long text used for testing subtitle preview.", @@ -320,10 +321,10 @@ def _initStyle(self): def __setValues(self): """设置初始值""" # 设置字幕排布 - key = cfg.get(cfg.subtitle_layout) - self.layoutCard.comboBox.setCurrentText(SubtitleLayoutEnum[key].value) + enum = cfg.get(cfg.subtitle_layout) # It should get the enum, not the name or value + self.layoutCard.comboBox.setCurrentText(enum.value) # 设置字幕样式 - self.styleNameComboBox.comboBox.setCurrentText(cfg.get(cfg.subtitle_style_name)) + self.styleNameComboBox.comboBox.setCurrentText(cfg.subtitle_style_name.value) # 获取系统字体,设置comboBox的选项 fontDatabase = QFontDatabase() @@ -354,7 +355,7 @@ def connectSignals(self): # 字幕排布 self.layoutCard.currentTextChanged.connect(self.onSettingChanged) self.layoutCard.currentTextChanged.connect( - lambda: cfg.set(cfg.subtitle_layout, self.getLayoutKey(self.layoutCard.comboBox.currentText()))) + lambda: cfg.set(cfg.subtitle_layout, SubtitleLayoutEnum(self.layoutCard.comboBox.currentText()))) # 垂直间距 self.verticalSpacingCard.spinBox.valueChanged.connect(self.onSettingChanged) @@ -388,12 +389,6 @@ def connectSignals(self): self.layoutCard.currentTextChanged.connect(signalBus.on_subtitle_layout_changed) signalBus.subtitle_layout_changed.connect(self.on_subtitle_layout_changed) - def getLayoutKey(self, value: str): - for item in SubtitleLayoutEnum: - if item.value == value: - return item.name - return SubtitleLayoutEnum.ONLY_TRANSLATE.name - def on_open_style_folder_clicked(self): """打开样式文件夹""" if sys.platform == "win32": @@ -404,7 +399,7 @@ def on_open_style_folder_clicked(self): subprocess.run(["xdg-open", SUBTITLE_STYLE_PATH]) def on_subtitle_layout_changed(self, layout: str): - cfg.subtitle_layout.value = self.getLayoutKey(layout) + cfg.subtitle_layout.value = SubtitleLayoutEnum(layout) self.layoutCard.comboBox.setCurrentText(layout) def onSettingChanged(self): @@ -485,11 +480,11 @@ def updatePreview(self): case SubtitleLayoutEnum.TRANSLATE_ON_TOP.value: main_text, sub_text = sub_text, main_text case SubtitleLayoutEnum.ORIGINAL_ON_TOP.value: - main_text, sub_text = main_text, sub_text + pass case SubtitleLayoutEnum.ONLY_TRANSLATE.value: main_text, sub_text = sub_text, None case SubtitleLayoutEnum.ONLY_ORIGINAL.value: - main_text, sub_text = main_text, None + sub_text = None # 创建预览线程 self.preview_thread = PreviewThread(style_str, (main_text, sub_text)) diff --git a/app/view/task_creation_interface.py b/app/view/task_creation_interface.py index c675088..a5bace4 100644 --- a/app/view/task_creation_interface.py +++ b/app/view/task_creation_interface.py @@ -6,7 +6,7 @@ from PyQt5.QtCore import pyqtSignal, Qt, QStandardPaths from PyQt5.QtGui import QPixmap, QDragEnterEvent, QDropEvent -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QApplication, QLabel, QFileDialog +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QApplication, QLabel, QFileDialog, QMessageBox from qfluentwidgets import LineEdit, ProgressBar, PushButton, InfoBar, InfoBarPosition, BodyLabel, ToolButton, HyperlinkButton from qfluentwidgets import FluentIcon, FluentStyleSheet, ComboBoxSettingCard from qfluentwidgets import FluentIcon as FIF @@ -287,7 +287,7 @@ def on_target_language_changed(self, language: str): self.target_language_card.comboBox.setCurrentText(language) def setup_values(self): - self.transcription_model_card.setValue(cfg.transcribe_model.value) + self.transcription_model_card.setValue(cfg.transcribe_model.value.value) self.target_language_card.setValue(cfg.target_language.value.value) self.subtitle_optimization_card.setChecked(cfg.need_optimize.value) self.subtitle_translation_card.setChecked(cfg.need_translate.value) @@ -340,7 +340,6 @@ def on_start_clicked(self): if self.start_button._icon == FluentIcon.FOLDER: if cfg.last_open_dir.value != "": open_path = cfg.last_open_dir.value - cfg.save() else: open_path = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation) @@ -361,15 +360,29 @@ def on_start_clicked(self): self.search_input.setText(file_path) return - + + if cfg.transcribe_model.value == TranscribeModelEnum.FASTER_WHISPER \ + and cfg.faster_whisper_one_word.value \ + and ( not cfg.api_base.value or not cfg.api_key.value ): + mbox = QMessageBox(self) + mbox.setWindowTitle(self.tr("API Base or API Key is not set.")) + mbox.setText(self.tr("You use FasterWhisper and using split word feature.\n" \ + + "It requires working LLM settings.\n" \ + + "So please set up the 'API Base' and 'API Key' values in Settings before start the process.")) + mbox.show() + return + need_whisper_settings = cfg.transcribe_model.value in [ TranscribeModelEnum.WHISPER, TranscribeModelEnum.WHISPER_API, TranscribeModelEnum.FASTER_WHISPER ] + + if need_whisper_settings and not self.show_whisper_settings(): return + self.process() def on_search_input_changed(self): @@ -434,7 +447,15 @@ def _is_valid_url(self, url): return False def _process_file(self, file_path): - self.create_task_thread = CreateTaskThread(file_path, Task.Type.SUBTITLE, cfg.soft_subtitle.value) + if self.subtitle_optimization_card.switchButton.isChecked(): + task_type = Task.Type.OPTIMIZE + elif self.subtitle_translation_card.switchButton.isChecked(): + task_type = Task.Type.TRANSLATE + else: + task_type = Task.Type.TRANSCRIBE + + self.create_task_thread = CreateTaskThread(file_path, task_type, 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() diff --git a/app/view/transcription_interface.py b/app/view/transcription_interface.py index 0357572..4888c5f 100644 --- a/app/view/transcription_interface.py +++ b/app/view/transcription_interface.py @@ -7,10 +7,11 @@ from pathlib import Path from PyQt5.QtCore import * -from PyQt5.QtGui import QPixmap, QFont, QDragEnterEvent, QDropEvent -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QApplication, QLabel, QFileDialog +from PyQt5.QtGui import QPixmap, QFont, QDragEnterEvent, QDropEvent, QColor +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QApplication, QLabel, QPlainTextEdit from qfluentwidgets import CardWidget, PrimaryPushButton, PushButton, InfoBar, BodyLabel, PillPushButton, setFont, \ ProgressRing, InfoBarPosition +from qfluentwidgets.common.config import isDarkTheme from ..components.FasterWhisperSettingDialog import FasterWhisperSettingDialog from ..components.WhisperSettingDialog import WhisperSettingDialog @@ -22,9 +23,53 @@ from ..core.entities import SupportedVideoFormats, SupportedAudioFormats from ..core.thread.transcript_thread import TranscriptThread from ..core.entities import TranscribeModelEnum +from ..common.signal_bus import signalBus DEFAULT_THUMBNAIL_PATH = RESOURCE_PATH / "assets" / "default_thumbnail.jpg" +class ProcessLogInfoCard(CardWidget): + console_line = pyqtSignal(Task) + task: Task|None = None + + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + self.setup_signals() + + def setup_ui(self): + self.cardLayout = QVBoxLayout(self) + self.cardLayout.setContentsMargins(20,15,20,15) + # Layout for log + self.log_toolbar_layout = QHBoxLayout(self) + self.log_label = BodyLabel(self) + self.log_clear_button = PushButton(self.tr("Clear"),self) + self.log_toolbar_layout.addWidget(self.log_label) + self.log_toolbar_layout.addStretch(1) + self.log_toolbar_layout.addWidget(self.log_clear_button) + self.log_label.setText(self.tr("程序日志:")) + + # Log area + self.process_log = QPlainTextEdit(self) + self.process_log.setReadOnly(True) + self.process_log.setMinimumWidth(200) + text_color = "#cccccc" if isDarkTheme() else "#000000" + self.process_log.setStyleSheet(f"QPlainTextEdit{{background:transparent; font-size:12px; color:{text_color}}}") + + self.cardLayout.addLayout(self.log_toolbar_layout) + self.cardLayout.addWidget(self.process_log) + + def addLine(self, text: str): + # After adding a message + self.process_log.appendPlainText(text) + self.process_log.verticalScrollBar().setValue(self.process_log.verticalScrollBar().maximum()) + + def clearLog(self): + self.process_log.clear() + + def setup_signals(self): + self.log_clear_button.clicked.connect(self.clearLog) + # Let other thread send lines to the log + signalBus.app_log_signal.connect(self.addLine) class VideoInfoCard(CardWidget): finished = pyqtSignal(Task) @@ -142,24 +187,26 @@ def setup_signals(self): def show_whisper_settings(self): """显示Whisper设置对话框""" - if cfg.transcribe_model.value == TranscribeModelEnum.WHISPER.value: - dialog = WhisperSettingDialog(self.window()) - if dialog.exec_(): - return True - elif cfg.transcribe_model.value == TranscribeModelEnum.WHISPER_API.value: - dialog = WhisperAPISettingDialog(self.window()) - if dialog.exec_(): - return True - elif cfg.transcribe_model.value == TranscribeModelEnum.FASTER_WHISPER.value: - dialog = FasterWhisperSettingDialog(self.window()) - if dialog.exec_(): - return True - return False + match cfg.transcribe_model.value.value: + case TranscribeModelEnum.WHISPER.value: + dialog = WhisperSettingDialog(self.window()) + if dialog.exec_(): + return True + case TranscribeModelEnum.WHISPER_API.value: + dialog = WhisperAPISettingDialog(self.window()) + if dialog.exec_(): + return True + case TranscribeModelEnum.FASTER_WHISPER.value: + dialog = FasterWhisperSettingDialog(self.window()) + if dialog.exec_(): + return True + case _: + return False def on_start_button_clicked(self): """开始转录按钮点击事件""" if self.task.status == Task.Status.TRANSCRIBING: - need_whisper_settings = cfg.transcribe_model.value in [ + need_whisper_settings = cfg.transcribe_model.value.value in [ TranscribeModelEnum.WHISPER.value, TranscribeModelEnum.WHISPER_API.value, TranscribeModelEnum.FASTER_WHISPER.value @@ -243,7 +290,7 @@ def on_transcript_finished(self, task): """转录完成处理""" self.start_button.setEnabled(True) self.start_button.setText(self.tr("转录完成")) - if self.task.status == Task.Status.PENDING: + if self.task.status not in [Task.Status.CANCELED, Task.Status.COMPLETED, Task.Status.FAILED]: self.finished.emit(task) def reset_ui(self): @@ -281,16 +328,14 @@ def _init_ui(self): self.main_layout = QVBoxLayout(self) self.main_layout.setObjectName("main_layout") self.main_layout.setSpacing(20) - + self.process_log_card = ProcessLogInfoCard(self) self.video_info_card = VideoInfoCard(self) - self.main_layout.addWidget(self.video_info_card) - self.file_select_button = PushButton(self.tr("选择视频文件"), self) - self.main_layout.addWidget(self.file_select_button, alignment=Qt.AlignmentFlag.AlignCenter) + self.main_layout.addWidget(self.video_info_card) + self.main_layout.addWidget(self.process_log_card) def _setup_signals(self): """设置信号连接""" - self.file_select_button.clicked.connect(self._on_file_select) self.video_info_card.finished.connect(self._on_transcript_finished) def _on_transcript_finished(self, task): @@ -304,32 +349,6 @@ def _on_transcript_finished(self, task): parent=self.parent() ) - def _on_file_select(self): - """文件选择处理""" - file_dialog = QFileDialog() - file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile) - - # 构建文件过滤器 - video_formats = " ".join(f"*.{fmt.value}" for fmt in SupportedVideoFormats) - audio_formats = " ".join(f"*.{fmt.value}" for fmt in SupportedAudioFormats) - filter_str = f"{self.tr('媒体文件')} ({video_formats} {audio_formats});;{self.tr('视频文件')} ({video_formats});;{self.tr('音频文件')} ({audio_formats})" - - if cfg.last_open_dir.value != "": - open_path = cfg.last_open_dir.value - cfg.save() - else: - open_path = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DesktopLocation) - - file_path, _ = file_dialog.getOpenFileName(self, self.tr("选择媒体文件"), open_path, filter_str) - if file_path: - # Save this file's directory for later use - file_dir = str( Path(file_path).parent ) - if file_dir != cfg.last_open_dir.value: - cfg.last_open_dir.value = file_dir - cfg.save() - - self.create_task(file_path) - def create_task(self, file_path): """创建任务""" self.create_task_thread = CreateTaskThread(file_path, Task.Type.TRANSCRIBE) diff --git a/main.py b/main.py index ccce17f..69c298c 100644 --- a/main.py +++ b/main.py @@ -6,8 +6,10 @@ """ import os import sys +import json import traceback from datetime import datetime +from app.config import RESOURCE_PATH, APPDATA_PATH # Add project root directory to Python path project_root = os.path.dirname(os.path.abspath(__file__)) @@ -21,15 +23,15 @@ for file in os.listdir(): if file.startswith("app") and file.endswith(".pyd"): os.remove(file) + from PyQt5.QtCore import Qt, QTranslator from PyQt5.QtWidgets import QApplication -from qfluentwidgets import FluentTranslator from app.common.config import cfg from app.common.enums import Enums_Translate from app.view.main_window import MainWindow -from app.config import RESOURCE_PATH + from app.core.utils import logger @@ -55,11 +57,9 @@ def exception_hook(exctype, value, tb): # Internationalization (Multi-language) locale = cfg.get(cfg.language).value -# translator = FluentTranslator(locale) myTranslator = QTranslator() translations_path = RESOURCE_PATH / "translations" / f"VideoCaptioner_{locale.name()}.qm" myTranslator.load(str(translations_path)) -# app.installTranslator(translator) app.installTranslator(myTranslator) # Set the enums to new translated values diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..6f57693 --- /dev/null +++ b/run.bat @@ -0,0 +1,2 @@ +call .\.venv\Scripts\activate.bat +python main.py \ No newline at end of file