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