From a8a8d4839ad801867e7589d536e9e53750581f7a Mon Sep 17 00:00:00 2001 From: jameson512 <2867557054@qq.com> Date: Fri, 10 Jan 2025 17:51:50 +0800 Subject: [PATCH] Feat: add glossary when translate use AI --- sp.py | 2 +- videotrans/__init__.py | 4 +- videotrans/glossary.txt | 3 + videotrans/mainwin/_actions_sub.py | 89 ---------------------- videotrans/mainwin/_main_win.py | 3 +- videotrans/task/trans_create.py | 2 - videotrans/translator/_base.py | 18 ++++- videotrans/tts/_edgetts.py | 2 + videotrans/ui/en.py | 30 ++++---- videotrans/ui/fanyi.py | 61 ++++++++++----- videotrans/ui/subtitle_editor.py | 4 +- videotrans/ui/vasrt.py | 116 ++++++++++++++++++++++++++++- videotrans/util/tools.py | 94 ++++++++++++++++++++++- videotrans/winform/fn_fanyisrt.py | 1 + videotrans/winform/fn_vas.py | 79 +++++++++++++++++--- 15 files changed, 358 insertions(+), 150 deletions(-) create mode 100644 videotrans/glossary.txt diff --git a/sp.py b/sp.py index 97b7e7e9..92519a12 100644 --- a/sp.py +++ b/sp.py @@ -8,7 +8,7 @@ License: GPL-V3 # 代码是一坨屎,但又不是不能跑O(∩_∩)O~ -# 代码越写越是坨屎,好烦 +# """ import multiprocessing diff --git a/videotrans/__init__.py b/videotrans/__init__.py index 62b276a9..efb7da84 100644 --- a/videotrans/__init__.py +++ b/videotrans/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -VERSION = "v3.43" -VERSION_NUM = 120343 +VERSION = "v3.44" +VERSION_NUM = 120344 diff --git a/videotrans/glossary.txt b/videotrans/glossary.txt new file mode 100644 index 00000000..b5bd0ad3 --- /dev/null +++ b/videotrans/glossary.txt @@ -0,0 +1,3 @@ +美国弹道导弹防御系统=NBMD +中国导弹防御系统=CNMD +开普特感冒药=OTC \ No newline at end of file diff --git a/videotrans/mainwin/_actions_sub.py b/videotrans/mainwin/_actions_sub.py index d1254cef..0cd36b68 100644 --- a/videotrans/mainwin/_actions_sub.py +++ b/videotrans/mainwin/_actions_sub.py @@ -96,93 +96,6 @@ def check_cuda(self, state): res = False self.cfg['cuda'] = res - # 简单新手模式 - def set_xinshoujandann(self): - - self.main.splitter.setSizes([self.main.width, 0]) - self.main.action_xinshoujandan.setChecked(True) - self.main.app_mode = 'biaozhun_jd' - self.main.show_tips.setText(config.transobj['xinshoumoshitips']) - self.main.startbtn.setText(config.transobj['kaishichuli']) - self.main.action_biaozhun.setChecked(False) - self.main.action_tiquzimu.setChecked(False) - - # 仅保存视频行 - self.main.only_video.setChecked(False) - self.main.only_video.hide() - self.main.copysrt_rawvideo.hide() - - # 翻译 - self.main.translate_type.setCurrentIndex(1) - self.main.label_9.hide() - self.main.translate_type.hide() - self.main.label_2.show() - self.main.source_language.show() - self.main.label_3.show() - self.main.target_language.show() - self.main.label.hide() - self.main.proxy.hide() - - # 配音角色 - self.main.tts_text.show() - self.main.tts_type.setCurrentIndex(0) - self.main.tts_type.setDisabled(True) - self.main.tts_type.show() - self.main.label_4.show() - self.main.voice_role.show() - self.main.listen_btn.show() - self.main.volume_rate.setDisabled(True) - self.main.volume_rate.show() - self.main.volume_label.show() - self.main.pitch_label.show() - self.main.pitch_rate.setDisabled(True) - self.main.pitch_rate.show() - - - # 语音识别行 - self.main.split_type.setCurrentIndex(0) - self.main.model_name.setCurrentIndex(0) - self.main.reglabel.hide() - self.main.recogn_type.setCurrentIndex(0) - self.main.recogn_type.hide() - self.main.model_name_help.hide() - self.main.model_name.hide() - self.main.split_label.hide() - self.main.split_type.hide() - self.main.subtitle_type.setCurrentIndex(1) - self.main.subtitle_type.hide() - self.main.rephrase.setChecked(False) - self.main.rephrase.hide() - self.main.remove_noise.setChecked(False) - self.main.remove_noise.hide() - - - - - # 字幕对齐行 - self.main.align_btn.hide() - self.main.label_6.hide() - self.main.voice_rate.hide() - self.main.append_video.setChecked(True) - self.main.append_video.hide() - self.main.voice_autorate.setChecked(True) - self.main.voice_autorate.hide() - self.main.video_autorate.setChecked(True) - self.main.video_autorate.hide() - self.main.is_separate.setChecked(False) - self.main.is_separate.hide() - self.main.enable_cuda.setChecked(False) - self.main.enable_cuda.hide() - self.main.label_cjklinenums.hide() - self.main.cjklinenums.hide() - self.main.label_othlinenums.hide() - self.main.othlinenums.hide() - # 添加背景行 - self.main.addbackbtn.hide() - self.main.back_audio.hide() - self.main.is_loop_bgm.hide() - self.main.bgmvolume_label.hide() - self.main.bgmvolume.hide() # 启用标准模式 @@ -192,7 +105,6 @@ def set_biaozhun(self): self.main.app_mode = 'biaozhun' self.main.show_tips.setText("自定义各项配置,批量进行视频翻译。选择单个视频时,处理过程中可暂停编辑字幕" if config.defaulelang=='zh' else 'Customize each configuration to batch video translation. When selecting a single video, you can pause to edit subtitles during processing.') self.main.startbtn.setText(config.transobj['kaishichuli']) - self.main.action_xinshoujandan.setChecked(False) self.main.action_tiquzimu.setChecked(False) # 仅保存视频行 @@ -274,7 +186,6 @@ def set_tiquzimu(self): self.main.app_mode = 'tiqu' self.main.show_tips.setText(config.transobj['tiquzimu']) self.main.startbtn.setText(config.transobj['kaishitiquhefanyi']) - self.main.action_xinshoujandan.setChecked(False) self.main.action_biaozhun.setChecked(False) # 仅保存视频行 diff --git a/videotrans/mainwin/_main_win.py b/videotrans/mainwin/_main_win.py index 823cf1cc..e5b2e626 100644 --- a/videotrans/mainwin/_main_win.py +++ b/videotrans/mainwin/_main_win.py @@ -144,7 +144,6 @@ def initUI(self): self.model_name.setDisabled(False) self.moshis = { - "biaozhun_jd": self.action_xinshoujandan, "biaozhun": self.action_biaozhun, "tiqu": self.action_tiquzimu } @@ -264,6 +263,7 @@ def _set_cache_set(self): self.hfaster_help.clicked.connect(lambda :tools.open_url(url='https://pyvideotrans.com/vad')) self.split_label.clicked.connect(lambda: tools.open_url(url='https://pyvideotrans.com/splitmode')) self.align_btn.clicked.connect(lambda: tools.open_url(url='https://pyvideotrans.com/align')) + self.glossary.clicked.connect(lambda:tools.show_glossary_editor(self)) def _start_subform(self): @@ -281,7 +281,6 @@ def _start_subform(self): from videotrans import winform - self.action_xinshoujandan.triggered.connect(self.win_action.set_xinshoujandann) self.action_biaozhun.triggered.connect(self.win_action.set_biaozhun) self.action_tiquzimu.triggered.connect(self.win_action.set_tiquzimu) diff --git a/videotrans/task/trans_create.py b/videotrans/task/trans_create.py index 872f4460..35de1478 100644 --- a/videotrans/task/trans_create.py +++ b/videotrans/task/trans_create.py @@ -734,8 +734,6 @@ def _novoicemp4_add_time(self, duration_ms): default_codec = f"libx{config.settings['video_codec']}" cmd = [ '-y', - "-threads", - f'{os.cpu_count()}', '-i', self.cfg['novoice_mp4'], '-vf', diff --git a/videotrans/translator/_base.py b/videotrans/translator/_base.py index f8111cdc..9f40936d 100644 --- a/videotrans/translator/_base.py +++ b/videotrans/translator/_base.py @@ -277,9 +277,21 @@ def runsrt(self): def _refine3_prompt(self): - zh_prompt=Path(config.ROOT_DIR+'/videotrans/prompts/srt/fansi3.txt').read_text(encoding='utf-8') - en_prompt=Path(config.ROOT_DIR+'/videotrans/prompts/srt/fansi3-en.txt').read_text(encoding='utf-8') - return zh_prompt if config.defaulelang=='zh' else en_prompt + glossary='' + if Path(config.ROOT_DIR+'/videotrans/glossary.txt').exists(): + glossary=Path(config.ROOT_DIR+'/videotrans/glossary.txt').read_text(encoding='utf-8').strip() + if config.defaulelang=='zh': + prompt=Path(config.ROOT_DIR+'/videotrans/prompts/srt/fansi3.txt').read_text(encoding='utf-8') + glossary_prompt="""## 术语表\n严格按照以下术语表进行翻译,如果句子中出现术语,必须使用对应的翻译,而不能自由翻译:\n| 术语 | 翻译 |\n| --------- | ----- |\n""" + else: + prompt=Path(config.ROOT_DIR+'/videotrans/prompts/srt/fansi3-en.txt').read_text(encoding='utf-8') + glossary_prompt="""## Glossary of terms\nTranslations are made strictly according to the following glossary. If a term appears in a sentence, the corresponding translation must be used, not a free translation:\n| Glossary | Translation |\n| --------- | ----- |\n""" + + if glossary: + glossary="\n".join(["|"+it.replace("=",'|')+"|" for it in glossary.split('\n')]) + prompt=prompt.replace('',f"""{glossary_prompt}{glossary}\n\n""") + + return prompt def _set_cache(self, it, res_str): if not res_str.strip(): diff --git a/videotrans/tts/_edgetts.py b/videotrans/tts/_edgetts.py index c12203c6..2bac387d 100644 --- a/videotrans/tts/_edgetts.py +++ b/videotrans/tts/_edgetts.py @@ -68,6 +68,8 @@ def process(): await communicate.stream() except Exception as e: config.logger.exception(e, exc_info=True) + if str(e).find('Invalid response status'): + raise Exception('可能被edge限流,请尝试使用或切换代理节点') print(f"异步合成出错: {e}") raise finally: diff --git a/videotrans/ui/en.py b/videotrans/ui/en.py index a1bcec13..e200d62a 100644 --- a/videotrans/ui/en.py +++ b/videotrans/ui/en.py @@ -144,6 +144,15 @@ def setupUi(self, MainWindow): self.horizontalLayout_5.addWidget(self.label_3) self.horizontalLayout_5.addWidget(self.target_language) + + self.glossary = QtWidgets.QPushButton(self.layoutWidget) + self.glossary.setMinimumSize(QtCore.QSize(0, 30)) + self.glossary.setObjectName("glossary") + self.glossary.setText("glossary" if config.defaulelang!='zh' else '术语表') + self.glossary.setStyleSheet("""background-color:transparent""") + self.glossary.setCursor(Qt.PointingHandCursor) + self.glossary.setToolTip('点击设置和修改术语表' if config.defaulelang=='zh' else 'Click to set up and modify the glossary') + self.label = QtWidgets.QPushButton(self.layoutWidget) self.label.setMinimumSize(QtCore.QSize(0, 30)) self.label.setObjectName("label") @@ -153,6 +162,10 @@ def setupUi(self, MainWindow): self.proxy.setMinimumSize(QtCore.QSize(0, 30)) self.proxy.setObjectName("proxy") + + + + self.horizontalLayout_5.addWidget(self.glossary) self.horizontalLayout_5.addWidget(self.label) self.horizontalLayout_5.addWidget(self.proxy) @@ -737,10 +750,7 @@ def setupUi(self, MainWindow): self.action_biaozhun.setChecked(True) self.action_biaozhun.setObjectName("action_biaozhun") - self.action_xinshoujandan = QtGui.QAction(MainWindow) - self.action_xinshoujandan.setCheckable(True) - self.action_xinshoujandan.setChecked(False) - self.action_xinshoujandan.setObjectName("action_xinshoujandan") + self.action_yuyinshibie = QtGui.QAction(MainWindow) @@ -925,7 +935,6 @@ def setupUi(self, MainWindow): self.menuBar.addAction(self.menu.menuAction()) self.menuBar.addAction(self.menu_H.menuAction()) - self.toolBar.addAction(self.action_xinshoujandan) self.toolBar.addAction(self.action_biaozhun) self.toolBar.addAction(self.action_tiquzimu) @@ -933,12 +942,7 @@ def setupUi(self, MainWindow): self.toolBar.addAction(self.action_fanyi) self.toolBar.addAction(self.action_yuyinhecheng) self.toolBar.addAction(self.action_yingyinhebing) - if config.defaulelang=='zh': - self.toolBar.addAction(self.actionvideoandaudio) - self.toolBar.addAction(self.actionvideoandsrt) - self.toolBar.addAction(self.actionsubtitlescover) - self.toolBar.addAction(self.actionformatcover) - self.toolBar.addAction(self.action_subtitleediter) + # 200ms后渲染文字 QTimer.singleShot(50, self.retranslateUi) @@ -1050,9 +1054,7 @@ def retranslateUi(self): self.action_biaozhun.setToolTip( '批量进行视频翻译,并可按照需求自定义所有配置选项' if config.defaulelang == 'zh' else 'Batch video translation with all configuration options customizable on demand') - self.action_xinshoujandan.setText(config.uilanglist.get("action_xinshoujandan")) - self.action_xinshoujandan.setToolTip( - '按照默认设置,一键将视频从一种语言翻译为另一种语言并嵌入字幕和配音' if config.defaulelang == 'zh' else 'Translate videos from one language to another and embed subtitles and voiceovers in one click.') + self.action_yuyinshibie.setText(config.uilanglist.get("Speech Recognition Text")) self.action_yuyinshibie.setToolTip( diff --git a/videotrans/ui/fanyi.py b/videotrans/ui/fanyi.py index 17776f75..c7564054 100644 --- a/videotrans/ui/fanyi.py +++ b/videotrans/ui/fanyi.py @@ -42,15 +42,6 @@ def setupUi(self, fanyisrt): self.fanyi_translate_type.setMinimumSize(QtCore.QSize(100, 30)) self.fanyi_translate_type.setObjectName("fanyi_translate_type") - self.aisendsrt=QtWidgets.QCheckBox() - self.aisendsrt.setText('发送完整字幕' if config.defaulelang=='zh' else 'Send full subtitles') - self.aisendsrt.setToolTip('当使用AI或Google翻译渠道时,可选以完整srt字幕格式发送请求,但可能出现较多空行' if config.defaulelang=='zh' else 'When using AI or Google translation channel, you can translate in srt format, but there may be more empty lines') - self.aisendsrt.setChecked(config.settings.get('aisendsrt')) - - self.refine3=QtWidgets.QCheckBox() - self.refine3.setText('三步反思法翻译' if config.defaulelang=='zh' else 'Three Steps to Reflection Translation') - self.refine3.setToolTip('当使用AI翻译渠道,并选中以完整srt字幕格式发送时,可启用三步反思翻译法' if config.defaulelang=='zh' else 'When using the AI translation channel and checking the box to send in full srt subtitle format, the three-step reflective translation method can be enabled') - self.refine3.setChecked(config.settings.get('refine3')) self.fanyi_model_list = QtWidgets.QComboBox() self.fanyi_model_list.setMinimumSize(QtCore.QSize(100, 30)) @@ -60,8 +51,6 @@ def setupUi(self, fanyisrt): self.horizontalLayout_18.addWidget(self.fanyi_translate_type) - self.horizontalLayout_18.addWidget(self.aisendsrt) - self.horizontalLayout_18.addWidget(self.refine3) self.horizontalLayout_18.addWidget(self.fanyi_model_list) self.label_source = QtWidgets.QLabel() @@ -92,6 +81,15 @@ def setupUi(self, fanyisrt): self.fanyi_target.setObjectName("fanyi_target") self.horizontalLayout_18.addWidget(self.fanyi_target) + + self.glossary = QtWidgets.QPushButton() + self.glossary.setMinimumSize(QtCore.QSize(100, 25)) + self.glossary.setObjectName("glossary") + self.glossary.setText("glossary" if config.defaulelang!='zh' else '术语表') + self.glossary.setToolTip('点击设置和修改术语表' if config.defaulelang=='zh' else 'Click to set up and modify the glossary') + ##self.glossary.setStyleSheet("""background-color:transparent""") + self.glossary.setCursor(Qt.PointingHandCursor) + self.out_format = QtWidgets.QComboBox() self.out_format.addItems([ @@ -106,6 +104,34 @@ def setupUi(self, fanyisrt): label_out.setText('输出' if config.defaulelang == 'zh' else 'Output') self.horizontalLayout_18.addWidget(label_out) self.horizontalLayout_18.addWidget(self.out_format) + self.horizontalLayout_18.addWidget(self.glossary) + self.horizontalLayout_18.addStretch() + + + + + + self.verticalLayout_13.addLayout(self.horizontalLayout_18) + + + + + + self.aisendsrt=QtWidgets.QCheckBox() + self.aisendsrt.setText('发送完整字幕' if config.defaulelang=='zh' else 'Send full subtitles') + self.aisendsrt.setToolTip('当使用AI或Google翻译渠道时,可选以完整srt字幕格式发送请求,但可能出现较多空行' if config.defaulelang=='zh' else 'When using AI or Google translation channel, you can translate in srt format, but there may be more empty lines') + self.aisendsrt.setChecked(config.settings.get('aisendsrt')) + + self.refine3=QtWidgets.QCheckBox() + self.refine3.setText('三步反思法翻译' if config.defaulelang=='zh' else 'Three Steps to Reflection Translation') + self.refine3.setToolTip('当使用AI翻译渠道,并选中以完整srt字幕格式发送时,可启用三步反思翻译法' if config.defaulelang=='zh' else 'When using the AI translation channel and checking the box to send in full srt subtitle format, the three-step reflective translation method can be enabled') + self.refine3.setChecked(config.settings.get('refine3')) + + + self.fanyi_proxy = QtWidgets.QLineEdit() + self.fanyi_proxy.setMinimumSize(QtCore.QSize(0, 30)) + self.fanyi_proxy.setObjectName("fanyi_proxy") + self.label_614 = QtWidgets.QLabel() sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) @@ -115,14 +141,13 @@ def setupUi(self, fanyisrt): self.label_614.setSizePolicy(sizePolicy) self.label_614.setMinimumSize(QtCore.QSize(0, 30)) self.label_614.setObjectName("label_614") - self.horizontalLayout_18.addWidget(self.label_614) - self.fanyi_proxy = QtWidgets.QLineEdit() - self.fanyi_proxy.setMinimumSize(QtCore.QSize(0, 30)) - self.fanyi_proxy.setObjectName("fanyi_proxy") - self.horizontalLayout_18.addWidget(self.fanyi_proxy) - - self.verticalLayout_13.addLayout(self.horizontalLayout_18) + self.horizontalLayout_new = QtWidgets.QHBoxLayout() + self.horizontalLayout_new.addWidget(self.aisendsrt) + self.horizontalLayout_new.addWidget(self.refine3) + self.horizontalLayout_new.addWidget(self.label_614) + self.horizontalLayout_new.addWidget(self.fanyi_proxy) + self.verticalLayout_13.addLayout(self.horizontalLayout_new) self.loglabel = QtWidgets.QPushButton() self.loglabel.setStyleSheet('''color:#148cd2;background-color:transparent''') diff --git a/videotrans/ui/subtitle_editor.py b/videotrans/ui/subtitle_editor.py index 6e43ac9b..3638a332 100644 --- a/videotrans/ui/subtitle_editor.py +++ b/videotrans/ui/subtitle_editor.py @@ -725,12 +725,12 @@ def save_ass(self, file_path,out_format=-1): bgcolor = self.qcolor_to_ass_color(self.selected_backgroundcolor, type='bg') bdcolor = self.qcolor_to_ass_color(self.selected_bordercolor, type='bd') fontcolor = self.qcolor_to_ass_color(self.selected_color, type='fc') - self.qcolor_to_ass_color(self.selected_color) + file.write( f'Style: Default,{self.selected_font.family()},{self.font_size_edit.text() if self.font_size_edit.text() else "20"},{fontcolor},{fontcolor},{bdcolor},{bgcolor},{int(self.selected_font.bold())},{int(self.selected_font.italic())},0,0,100,100,0,0,1,1,0,2,{left},{right},{vbottom},1\n') file.write("\n[Events]\n") file.write("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n") - self.selected_font.bold() + index = 1 for i in range(self.content_layout.count()): diff --git a/videotrans/ui/vasrt.py b/videotrans/ui/vasrt.py index c3ab1c86..43d9f242 100644 --- a/videotrans/ui/vasrt.py +++ b/videotrans/ui/vasrt.py @@ -3,8 +3,9 @@ from pathlib import Path from PySide6 import QtCore, QtWidgets -from PySide6.QtCore import (QMetaObject) -from PySide6.QtWidgets import (QHBoxLayout) +from PySide6.QtCore import QMetaObject,Qt, QTime, QTimer, QSize, QEvent +from PySide6.QtWidgets import QHBoxLayout,QFontDialog,QColorDialog, QTimeEdit +from PySide6.QtGui import QFont, QColor, QDragEnterEvent, QDropEvent from videotrans.configure import config @@ -106,7 +107,8 @@ def setupUi(self, vasrt): self.audio_process = QtWidgets.QComboBox() self.audio_process.addItems([ "截断" if config.defaulelang == 'zh' else "Truncate", - "自动加速" if config.defaulelang == 'zh' else "Auto Accelerate" + "音频加速" if config.defaulelang == 'zh' else "Auto Accelerate", + "视频末尾定格" if config.defaulelang == 'zh' else "Video copy", ]) @@ -150,6 +152,51 @@ def setupUi(self, vasrt): self.v3.addLayout(self.h6) self.v3.addLayout(self.h7) + + self.font_button = QtWidgets.QPushButton("选择字体" if config.defaulelang == 'zh' else 'Select Fonts') + self.font_button.setToolTip('点击选择字体' if config.defaulelang == 'zh' else 'Click it for select fonts') + self.font_button.clicked.connect(self.choose_font) + self.font_button.setCursor(Qt.PointingHandCursor) + + self.color_button = QtWidgets.QPushButton("字体颜色" if config.defaulelang == 'zh' else 'Text Colors') + self.color_button.setCursor(Qt.PointingHandCursor) + self.color_button.clicked.connect(self.choose_color) + + self.backgroundcolor_button = QtWidgets.QPushButton("背景色" if config.defaulelang == 'zh' else 'Backgroud Colors') + self.backgroundcolor_button.setCursor(Qt.PointingHandCursor) + self.backgroundcolor_button.clicked.connect(self.choose_backgroundcolor) + self.backgroundcolor_button.setToolTip( + '不同播放器下可能不起作用' if config.defaulelang == 'zh' else 'May not work in different players') + + self.bordercolor_button = QtWidgets.QPushButton("边框色" if config.defaulelang == 'zh' else 'Backgroud Colors') + self.bordercolor_button.setCursor(Qt.PointingHandCursor) + self.bordercolor_button.clicked.connect(self.choose_bordercolor) + self.bordercolor_button.setToolTip( + '不同播放器下可能不起作用' if config.defaulelang == 'zh' else 'May not work in different players') + + self.font_size_edit = QtWidgets.QLineEdit() + self.font_size_edit.setFixedWidth(80) + self.font_size_edit.setText('16') + self.font_size_edit.setPlaceholderText("字体大小" if config.defaulelang == 'zh' else 'Font Size') + self.font_size_edit.setToolTip("字体大小" if config.defaulelang == 'zh' else 'Font Size') + + # 初始化字体和颜色 + self.selected_font = QFont('Arial', 16) # 默认字体 + self.selected_color = QColor('#FFFFFFFF') # 默认颜色 + self.selected_backgroundcolor = QColor('#00000000') # 默认颜色 + self.selected_bordercolor = QColor('#00000000') # 默认颜色 + + format_layout = QHBoxLayout() + format_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + + format_layout.addWidget(self.font_button) + format_layout.addWidget(self.font_size_edit) + format_layout.addWidget(self.color_button) + format_layout.addWidget(self.backgroundcolor_button) + format_layout.addWidget(self.bordercolor_button) + + self.v3.addLayout(format_layout) + self.ysphb_startbtn = QtWidgets.QPushButton() self.ysphb_startbtn.setMinimumSize(QtCore.QSize(250, 40)) self.ysphb_startbtn.setObjectName("ysphb_startbtn") @@ -176,6 +223,63 @@ def setupUi(self, vasrt): QMetaObject.connectSlotsByName(vasrt) + def qcolor_to_ass_color(self, color, type='fc'): + # 获取颜色的 RGB 值 + r = color.red() + g = color.green() + b = color.blue() + if type in ['bg', 'bd']: + return f"&H80{b:02X}{g:02X}{r:02X}" + # 将 RGBA 转换为 ASS 的颜色格式 &HBBGGRR + return f"&H{b:02X}{g:02X}{r:02X}" + + def choose_font(self): + + dialog = QFontDialog(self.selected_font, self) + if dialog.exec(): + font = dialog.selectedFont() + font_name = font.family() + font_size = font.pointSize() + self.selected_font = font + self.font_size_edit.setText(str(font_size)) + self.font_button.setText(font_name) + self._setfont() + + def _setfont(self): + bgcolor = self.selected_backgroundcolor.name() + bgcolor = '' if bgcolor == '#000000' else f'background-color:{bgcolor}' + bdcolor = self.selected_bordercolor.name() + bdcolor = '' if bdcolor == '#000000' else f'border:1px solid {bdcolor}' + color = self.selected_color.name() + color = '' if color == '#000000' else f'color:{color}' + font = self.selected_font + self.font_button.setStyleSheet( + f"""font-family:'{font.family()}';font-size:{font.pointSize()}px;font-weight:{700 if font.bold() else 400};font-style:{'normal' if font.italic() else 'italic'};{bgcolor};{color};{bdcolor}""") + + def choose_color(self): + dialog = QColorDialog(self.selected_color, self) + dialog.setOption(QColorDialog.ShowAlphaChannel, True) # 启用透明度选择 + color = dialog.getColor() + + if color.isValid(): + self.selected_color = color + self._setfont() + + def choose_backgroundcolor(self): + dialog = QColorDialog(self.selected_backgroundcolor, self) + dialog.setOption(QColorDialog.ShowAlphaChannel, True) # 启用透明度选择 + color = dialog.getColor() + if color.isValid(): + self.selected_backgroundcolor = color + self._setfont() + def choose_bordercolor(self): + dialog = QColorDialog(self.selected_bordercolor, self) + dialog.setOption(QColorDialog.ShowAlphaChannel, True) # 启用透明度选择 + color = dialog.getColor() + if color.isValid(): + self.selected_bordercolor = color + self._setfont() + def remainraw(self, t): if Path(t).is_file(): self.ysphb_replace.setDisabled(False) @@ -188,6 +292,12 @@ def update_language(self, state): self.languagelabel.setStyleSheet(f"""color:#f1f1f1""" if state else 'color:#777777') self.language.setDisabled(False if state else True) + self.font_button.setDisabled(True if state else False) + self.font_size_edit.setDisabled(True if state else False) + self.color_button.setDisabled(True if state else False) + self.backgroundcolor_button.setDisabled(True if state else False) + self.bordercolor_button.setDisabled(True if state else False) + def retranslateUi(self, vasrt): vasrt.setWindowTitle("视频、音频、字幕三者合并" if config.defaulelang == 'zh' else 'Video, audio, and subtitle merging') diff --git a/videotrans/util/tools.py b/videotrans/util/tools.py index 39fa669d..f355d3b7 100644 --- a/videotrans/util/tools.py +++ b/videotrans/util/tools.py @@ -1813,7 +1813,15 @@ def format_video(name, target_dir=None): # 获取 prompt提示词 def get_prompt(ainame,is_srt=True): prompt_file=get_prompt_file(ainame=ainame,is_srt=is_srt) - return Path(prompt_file).read_text(encoding='utf-8') + content=Path(prompt_file).read_text(encoding='utf-8') + glossary='' + if Path(config.ROOT_DIR+'/videotrans/glossary.txt').exists(): + glossary=Path(config.ROOT_DIR+'/videotrans/glossary.txt').read_text(encoding='utf-8').strip() + if glossary: + glossary="\n".join(["|"+it.replace("=",'|')+"|" for it in glossary.split('\n')]) + glossary_prompt="""## 术语表\n严格按照以下术语表进行翻译,如果句子中出现术语,必须使用对应的翻译,而不能自由翻译:\n| 术语 | 翻译 |\n| --------- | ----- |\n""" if config.defaulelang=='zh' else """## Glossary of terms\nTranslations are made strictly according to the following glossary. If a term appears in a sentence, the corresponding translation must be used, not a free translation:\n| Glossary | Translation |\n| --------- | ----- |\n""" + content=content.replace('',f"""{glossary_prompt}{glossary}\n\n""") + return content # 获取当前需要操作的prompt txt文件 def get_prompt_file(ainame,is_srt=True): @@ -1944,4 +1952,86 @@ def check_local_api(api): msg_box.setInformativeText('请将 0.0.0.0 改为 127.0.0.1 ' if config.defaulelang == 'zh' else 'Please change 0.0.0.0 to 127.0.0.1. ') msg_box.exec() return False - return True \ No newline at end of file + return True + +def format_milliseconds(milliseconds): + """ + 将毫秒数转换为 HH:mm:ss.zz 格式的字符串。 + + Args: + milliseconds (int): 毫秒数。 + + Returns: + str: 格式化后的字符串,格式为 HH:mm:ss.zz。 + """ + if not isinstance(milliseconds, int): + raise TypeError("毫秒数必须是整数") + if milliseconds < 0: + raise ValueError("毫秒数必须是非负整数") + + seconds = milliseconds / 1000 + + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + milliseconds_part = int((seconds * 1000) % 1000)//10 # 保留两位 + + # 格式化为两位数字字符串 + formatted_hours = f"{int(hours):02}" + formatted_minutes = f"{int(minutes):02}" + formatted_seconds = f"{int(seconds):02}" + formatted_milliseconds = f"{milliseconds_part:02}" + + print(f"{milliseconds=},{formatted_hours}:{formatted_minutes}:{formatted_seconds}.{formatted_milliseconds}") + + return f"{formatted_hours}:{formatted_minutes}:{formatted_seconds}.{formatted_milliseconds}" + +def show_glossary_editor(parent): + from PySide6.QtWidgets import (QApplication, QWidget, QPushButton, + QVBoxLayout, QTextEdit, QDialog, + QHBoxLayout, QDialogButtonBox) + from PySide6.QtCore import Qt + """ + 弹出一个窗口,包含一个文本框和保存按钮,并处理文本的读取和保存。 + + Args: + parent: 父窗口 (QWidget) + """ + dialog = QDialog(parent) + dialog.setWindowTitle("在此填写术语对照表,格式: 术语=翻译" if config.defaulelang=='zh' else '') + dialog.setMinimumSize(600, 400) + + layout = QVBoxLayout(dialog) + + text_edit = QTextEdit() + text_edit.setPlaceholderText("请按照 术语=翻译 的格式,一行一组来填写,例如\n\n国家弹道导弹防御系统=NBMD\n首席执行官=CEO\n人工智能=AI\n\n在原文中如果遇到以上左侧文字,则翻译结果使用右侧文字" if config.defaulelang=='zh' else "Please fill in one line at a time, following the term on the left and the translation on the right, e.g. \nBallistic Missile Defense=BMD\nChief Executive Officer=CEO") + layout.addWidget(text_edit) + + button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) + layout.addWidget(button_box) + + #读取文件内容,并设置为文本框默认值 + file_path = config.ROOT_DIR+"/videotrans/glossary.txt" + try: + if os.path.exists(file_path): + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + text_edit.setText(content) + except Exception as e: + print(f"读取文件失败: {e}") + + def save_text(): + """ + 点击保存按钮,将文本框内容写回文件。 + """ + try: + with open(file_path, "w", encoding="utf-8") as f: + f.write(text_edit.toPlainText()) # toPlainText 获取纯文本 + dialog.accept() + except Exception as e: + print(f"写入文件失败: {e}") + + + button_box.accepted.connect(save_text) + button_box.rejected.connect(dialog.reject) + dialog.setWindowModality(Qt.WindowModality.ApplicationModal) # 设置模态窗口 + dialog.exec() # 显示模态窗口 diff --git a/videotrans/winform/fn_fanyisrt.py b/videotrans/winform/fn_fanyisrt.py index 1f55a44b..e588dfa2 100644 --- a/videotrans/winform/fn_fanyisrt.py +++ b/videotrans/winform/fn_fanyisrt.py @@ -374,6 +374,7 @@ def export_srt(): winobj.fanyi_model_list.currentTextChanged.connect(model_change) winobj.loglabel.clicked.connect(show_detail_error) winobj.exportsrt.clicked.connect(export_srt) + winobj.glossary.clicked.connect(lambda:tools.show_glossary_editor(winobj)) winobj.show() diff --git a/videotrans/winform/fn_vas.py b/videotrans/winform/fn_vas.py index b6ca6d3c..b3e63b43 100644 --- a/videotrans/winform/fn_vas.py +++ b/videotrans/winform/fn_vas.py @@ -105,6 +105,24 @@ def run(self): '[aout]', '-ac', '2', tmp_mp4]) + self.audio=tmp_mp4 + audio_time = int(tools.get_audio_time(self.audio) * 1000) + if self.audio_process==2 and audio_time>video_time: + sec=(audio_time-video_time)/1000 + tmp_mp4 = config.TEMP_HOME + f"/{time.time()}.mp4" + cmd = [ + '-y', + '-i', + self.video, + '-vf', + f'tpad=stop_mode=clone:stop_duration={sec}', + "-an", + '-c:v', + 'copy' if Path(self.video).suffix.lower() == '.mp4' else 'libx264', + tmp_mp4 + ] + tools.runffmpeg(cmd) + self.video=tmp_mp4 # 视频和音频混合 # 如果存在字幕则生成中间结果end_mp4 @@ -115,7 +133,7 @@ def run(self): '-i', os.path.normpath(self.video), '-i', - os.path.normpath(tmp_mp4 if tmp_mp4 else self.audio), + os.path.normpath(self.audio), '-c:v', 'copy' if Path(self.video).suffix.lower() == '.mp4' else 'libx264', "-c:a", @@ -144,22 +162,24 @@ def run(self): ] if not self.is_soft or not self.language: # 硬字幕 - sub_list = tools.get_subtitle_from_srt(self.srt, is_file=True) - text = "" - for i, it in enumerate(sub_list): - it['text'] = textwrap.fill(it['text'], self.maxlen, replace_whitespace=False).strip() - text += f"{it['line']}\n{it['time']}\n{it['text'].strip()}\n\n" - srtfile = config.TEMP_HOME + f"/vasrt{time.time()}.srt" - with Path(srtfile).open('w', encoding='utf-8') as f: - f.write(text) - f.flush() - assfile = tools.set_ass_font(srtfile) + # sub_list = tools.get_subtitle_from_srt(self.srt, is_file=True) + # text = "" + # for i, it in enumerate(sub_list): + # it['text'] = textwrap.fill(it['text'], self.maxlen, replace_whitespace=False).strip() + # text += f"{it['line']}\n{it['time']}\n{it['text'].strip()}\n\n" + # srtfile = config.TEMP_HOME + f"/vasrt{time.time()}.srt" + # with Path(srtfile).open('w', encoding='utf-8') as f: + # f.write(text) + # f.flush() + # assfile = tools.set_ass_font(srtfile) + assfile=config.TEMP_HOME + f"/vasrt{time.time()}.ass" + save_ass(self.srt,assfile) os.chdir(config.TEMP_HOME) cmd += [ '-c:v', 'libx264', '-vf', - f"subtitles={os.path.basename(assfile)}", + f"subtitles={os.path.basename(assfile)}:charenc=utf-8", '-crf', f'{config.settings["crf"]}', '-preset', @@ -188,6 +208,41 @@ def run(self): else: self.post(type='ok', text=self.file) + + def save_ass(file_path,ass_file): + with open(ass_file, 'w', encoding='utf-8') as file: + # 写入 ASS 文件的头部信息 + stem = Path(file_path).stem + file.write("[Script Info]\n") + file.write(f"Title: {stem}\n") + file.write(f"Original Script: {stem}\n") + file.write("ScriptType: v4.00+\n") + file.write("PlayResX: 384\nPlayResY: 288\n") + file.write("ScaledBorderAndShadow: yes\n") + file.write("YCbCr Matrix: None\n") + file.write("\n[V4+ Styles]\n") + file.write( + f"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n") + left, right, vbottom = 10, 10, 10 + + + bgcolor = winobj.qcolor_to_ass_color(winobj.selected_backgroundcolor, type='bg') + bdcolor = winobj.qcolor_to_ass_color(winobj.selected_bordercolor, type='bd') + fontcolor = winobj.qcolor_to_ass_color(winobj.selected_color, type='fc') + + file.write( + f'Style: Default,{winobj.selected_font.family()},{winobj.font_size_edit.text() if winobj.font_size_edit.text() else "20"},{fontcolor},{fontcolor},{bdcolor},{bgcolor},{int(winobj.selected_font.bold())},{int(winobj.selected_font.italic())},0,0,100,100,0,0,1,1,0,2,{left},{right},{vbottom},1\n') + file.write("\n[Events]\n") + # 'Style: Default,{fontname},{fontsize},{fontcolor},&HFFFFFF,{fontbordercolor},{fontbackcolor},0,0,0,0,100,100,0,0,1,1,0,2,10,10,{subtitle_bottom},1' + file.write("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n") + srt_list=tools.get_subtitle_from_srt(file_path,is_file=True) + for it in srt_list: + start_str=tools.format_milliseconds(it['start_time']) + end_str=tools.format_milliseconds(it['end_time']) + text=it['text'].replace("\n","\\N") + file.write(f"Dialogue: 0,{start_str},{end_str},Default,,0,0,0,,{text}\n") + return True + def feed(d): if winobj.has_done: return