diff --git a/.gitignore b/.gitignore index 328ce7e..b29f43f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.pyc */__pycache__/ *.env +.* # 测试和脚本 /test/ @@ -24,4 +25,4 @@ # 应用数据 /AppData/ /output/ -/work-dir/ \ No newline at end of file +/work-dir/ diff --git a/app/common/config.py b/app/common/config.py index 0f67f82..c3965f5 100644 --- a/app/common/config.py +++ b/app/common/config.py @@ -200,6 +200,12 @@ class Config(QConfig): 1500, RangeValidator(500, 3000) ) + time_offset = RangeConfigItem( + "Subtitle", "TimeOffset", + 0, + RangeValidator(-5000, 5000) + ) + # ------------------- 软件页面配置 ------------------- micaEnabled = ConfigItem("MainWindow", "MicaEnabled", False, BoolValidator()) dpiScale = OptionsConfigItem( diff --git a/app/core/bk_asr/ASRData.py b/app/core/bk_asr/ASRData.py index d613603..bb28ba7 100644 --- a/app/core/bk_asr/ASRData.py +++ b/app/core/bk_asr/ASRData.py @@ -479,7 +479,7 @@ def from_vtt(vtt_str: str) -> 'ASRData': """ segments = [] # 跳过头部元数据 - content = vtt_str.split('\n\n')[2:] + content = vtt_str.split('\n\n')[1:] timestamp_pattern = re.compile(r'(\d{2}):(\d{2}):(\d{2})\.(\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})\.(\d{3})') diff --git a/app/core/subtitle_processor/optimizer.py b/app/core/subtitle_processor/optimizer.py index 52a2fe5..d84c569 100644 --- a/app/core/subtitle_processor/optimizer.py +++ b/app/core/subtitle_processor/optimizer.py @@ -2,7 +2,7 @@ import logging import os from concurrent.futures import ThreadPoolExecutor -import re +import re, json from typing import Dict import retry @@ -12,7 +12,8 @@ TRANSLATE_PROMPT, OPTIMIZER_PROMPT, REFLECT_TRANSLATE_PROMPT, - SINGLE_TRANSLATE_PROMPT + SINGLE_TRANSLATE_PROMPT, + SINGLE_BATCH_TRANSLATE_PROMPT ) from ..subtitle_processor.aligner import SubtitleAligner from ..utils import json_repair @@ -86,20 +87,29 @@ def optimizer_multi_thread(self, subtitle_json: Dict[int, str], chunks = [dict(items[i:i + batch_num]) for i in range(0, len(items), batch_num)] def process_chunk(chunk): + failed = False if translate: try: result = self.translate(chunk, reflect) except Exception as e: logger.error(f"翻译失败,使用单条翻译:{e}") - result = self.translate_single(chunk) + # logger.info(f"chunk len:{len(chunk)}\n{chunk}") + failed = True + # result = self.translate_single(chunk) else: try: result = self.optimize(chunk) except Exception as e: logger.error(f"优化失败:{e}") result = chunk + + if failed: + result = self.translate_single(chunk) + failed = False + if callback: - callback(result) + if isinstance(result, Dict): + callback(result) return result results = list(self.executor.map(process_chunk, chunks)) @@ -108,7 +118,7 @@ def process_chunk(chunk): optimizer_result = {k: v for result in results for k, v in result.items()} return optimizer_result - @retry.retry(tries=2) + @retry.retry(tries=1) def optimize(self, original_subtitle: Dict[int, str]) -> Dict[int, str]: """ Optimize the given subtitle. """ logger.info(f"[+]正在优化字幕:{next(iter(original_subtitle))} - {next(reversed(original_subtitle))}") @@ -133,7 +143,7 @@ def optimize(self, original_subtitle: Dict[int, str]) -> Dict[int, str]: self.llm_result_logger.info("===========") return aligned_subtitle - @retry.retry(tries=2) + @retry.retry(tries=1) def translate(self, original_subtitle: Dict[int, str], reflect=False) -> Dict[int, str]: """优化并翻译给定的字幕。""" if reflect: @@ -153,6 +163,7 @@ def _reflect_translate(self, original_subtitle: Dict[int, str]): # print(response_content) optimized_text = {k: v["optimized_subtitle"] for k, v in response_content.items()} # 字幕文本 aligned_subtitle = repair_subtitle(original_subtitle, optimized_text) # 修复字幕对齐问题 + # print(aligned_subtitle) # 在 translations 中查找对应的翻译 文本-翻译 映射 translations = {item["optimized_subtitle"]: item["revised_translation"] for item in response_content.values()} @@ -212,70 +223,65 @@ def _create_optimizer_message(self, original_subtitle): {"role": "user", "content": input_content}] return message - def translate_single(self, original_subtitle: Dict[int, str]) -> Dict[int, str]: + def translate_single(self, original_subtitles: Dict[int, str]) -> Dict[int, str]: """单条字幕翻译,用于在批量翻译失败时的备选方案""" translate_result = {} - for key, value in original_subtitle.items(): - try: - message = [{"role": "system", - "content": SINGLE_TRANSLATE_PROMPT.replace("[TargetLanguage]", self.target_language)}, - {"role": "user", "content": value}] - response = self.client.chat.completions.create( - model=self.model, - stream=False, - messages=message) - translate = response.choices[0].message.content.replace("\n", "") - original_text = self.remove_punctuation(value) - translated_text = self.remove_punctuation(translate) - translate_result[key] = f"{original_text}\n{translated_text}" - logger.info(f"单条翻译结果: {translate_result[key]}") - except Exception as e: - logger.error(f"单条翻译失败: {e}") - translate_result[key] = f"{value}\n " + # logger.info(f"org sub:{original_subtitle}") + for key, value in original_subtitles.items(): + # try: + message = [{"role": "system", + "content": SINGLE_TRANSLATE_PROMPT.replace("[TargetLanguage]", self.target_language)}, + {"role": "user", "content": [value]}] + response = self.client.chat.completions.create( + model=self.model, + stream=False, + messages=message) + logger.info(f"response: {response}\n") + translate = response.choices[0].message.content.replace("\n", "") + original_text = self.remove_punctuation(value) + translated_text = self.remove_punctuation(translate) + translate_result[key] = f"{original_text}\n{translated_text}" + logger.info(f"单条翻译结果: {translate_result[key]}") + # except Exception as e: + # logger.error(f"单条翻译失败: {e.with_traceback()}") + # translate_result[key] = f"{value}\n " return translate_result def translate_single_batch(self, original_subtitle: Dict[int,str], callback = None) -> Dict[int,str]: """直接大批翻译字幕""" translate_result = {} - text = "" - - i, total_lines = 1, len(original_subtitle) # line numbers starts with 1, not 0 - logger.info(f"total lines:{total_lines}") - reSearch = re.compile(r'^\[\[(\d+)\]\](.*)', re.MULTILINE) # Compile it so it can run faster - - """try:""" - while i <= total_lines: - text = "" - for j in range(self.batch_num): # Do it 10 sentence at a time - text += f"\n[[{i}]]{original_subtitle[str(i)]}" - i += 1 - if i > total_lines: # Reach the end - break - - logger.info(f"Translating lines up to {i}") - # logger.info(text) + previous_sentence = "" + previous_translation = "" + for key, value in original_subtitle.items(): + + content = SINGLE_BATCH_TRANSLATE_PROMPT.replace("[TargetLanguage]", self.target_language + ).replace( "[PreviousSentence]", previous_sentence + ).replace( "[PreviousTranslation]", previous_translation) + + # logger.info(f"prompt:{content}") + message = [{"role": "system", - "content": SINGLE_TRANSLATE_PROMPT.replace("[TargetLanguage]", self.target_language)}, - {"role": "user", "content": text}] + "content": content}, + {"role": "user", "content": value}] + + previous_sentence = value + response = self.client.chat.completions.create( model=self.model, stream=False, messages=message) - translate = response.choices[0].message.content - # logger.info("returned text:\n" + translate) - result_lines = reSearch.findall(translate) - # Add it to result dict - seg = {} - for j in range(len(result_lines)): - line:str = result_lines[j][0] - if line.isdigit(): - seg[line] = result_lines[j][1] + + return_text = response.choices[0].message.content + # logger.info(f"response:{type(return_text)}") + previous_translation = return_text + + line = {str(key): return_text} # Create a dictionary with key and translated text if callback: # report the progress - callback(seg) + callback(line) - translate_result.update(seg) # Add seg to result + translate_result.update(line) # Add line to result """ except Exception as e: logger.error(f"批量单句翻译失败{e}") diff --git a/app/core/subtitle_processor/split_by_llm.py b/app/core/subtitle_processor/split_by_llm.py index 864692f..5199ceb 100644 --- a/app/core/subtitle_processor/split_by_llm.py +++ b/app/core/subtitle_processor/split_by_llm.py @@ -67,7 +67,7 @@ def split_by_llm(text: str, model: str = "gpt-4o-mini", use_cache: bool = False, max_word_count_cjk: int = 18, - max_word_count_english: int = 12) -> List[str]: + max_word_count_english: int = 32) -> List[str]: """ 包装 split_by_llm_retry 函数,确保在重试全部失败后返回空列表 """ diff --git a/app/core/subtitle_processor/subtitle_config.py b/app/core/subtitle_processor/subtitle_config.py index b913797..3c4f5a1 100644 --- a/app/core/subtitle_processor/subtitle_config.py +++ b/app/core/subtitle_processor/subtitle_config.py @@ -296,6 +296,13 @@ SINGLE_TRANSLATE_PROMPT = """ You are a professional [TargetLanguage] translator. Please translate the following text into [TargetLanguage]. -Strictly maintain one-to-one correspondence for each line with the translation. +Return the translation result directly without any explanation or other content. +""" + +SINGLE_BATCH_TRANSLATE_PROMPT = """ +You are a professional [TargetLanguage] translator. +The previous sentence is: "[PreviousSentence]" +and it was translated as: "[PreviousTranslation]". +Please translate the following text into [TargetLanguage] and don't repeat the previous translation. Return the translation result directly without any explanation or other content. """ diff --git a/app/core/thread/create_task_thread.py b/app/core/thread/create_task_thread.py index 6df2e1f..a9bdf79 100644 --- a/app/core/thread/create_task_thread.py +++ b/app/core/thread/create_task_thread.py @@ -88,7 +88,7 @@ def create_file_task(self, file_path): if video_info.audio_codec in ["aac", "mp3", "pcm"]: audio_format = "copy" - if cfg.subtitle_output_format.value.value == "ass" and ass_style_path.exists(): + if cfg.subtitle_output_format.value.value == "ass": ass_style_name = cfg.subtitle_style_name.value ass_style_path = SUBTITLE_STYLE_PATH / f"{ass_style_name}.txt" subtitle_style_srt = ass_style_path.read_text(encoding="utf-8") @@ -353,6 +353,7 @@ def create_transcription_task(self, file_path): original_subtitle_save_path=str(original_subtitle_save_path), result_subtitle_save_path=str(result_subtitle_save_path), subtitle_style_srt=subtitle_style_srt, + 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, # Added by Philip diff --git a/app/core/thread/subtitle_optimization_thread.py b/app/core/thread/subtitle_optimization_thread.py index 9e7fbb2..ec8d1ac 100644 --- a/app/core/thread/subtitle_optimization_thread.py +++ b/app/core/thread/subtitle_optimization_thread.py @@ -129,18 +129,17 @@ def run(self): asr_data = from_subtitle_file(str_path) # 检查是否需要合并重新断句 - if need_optimize: - if not asr_data.is_word_timestamp() and need_split and self.task.faster_whisper_one_word: - asr_data.split_to_word_segments() - if asr_data.is_word_timestamp(): - self.progress.emit(15, self.tr("字幕断句...")) - logger.info("正在字幕断句...") - asr_data = merge_segments(asr_data, model=llm_model, - num_threads=thread_num, - max_word_count_cjk=max_word_count_cjk, - max_word_count_english=max_word_count_english) - asr_data.save(save_path=split_path) - self.update_all.emit(asr_data.to_json()) + if not asr_data.is_word_timestamp() and need_split and self.task.faster_whisper_one_word: + asr_data.split_to_word_segments() + if asr_data.is_word_timestamp(): + self.progress.emit(15, self.tr("字幕断句...")) + logger.info("正在字幕断句...") + asr_data = merge_segments(asr_data, model=llm_model, + num_threads=thread_num, + max_word_count_cjk=max_word_count_cjk, + 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()} @@ -182,10 +181,10 @@ def run(self): ) optimizer_result = self.optimizer.translate_single_batch(subtitle_json, callback=self.callback) - # 替换优化或者翻译后的字幕 + # 加入优化或者翻译后的字幕 for i, subtitle_text in optimizer_result.items(): seg = asr_data.segments[int(i) - 1] - seg.text = subtitle_text + seg.text = seg.text + "\n" + subtitle_text # 保存字幕 if result_subtitle_save_path.endswith(".ass"): diff --git a/app/core/thread/transcript_thread.py b/app/core/thread/transcript_thread.py index cd3abfe..d74aa36 100644 --- a/app/core/thread/transcript_thread.py +++ b/app/core/thread/transcript_thread.py @@ -162,17 +162,23 @@ def run(self): asr_data = self.asr.run(callback=self.progress_callback) # Check if asr_data needs to add minimum length - if cfg.subtitle_enable_sentence_minimum_time: + if cfg.subtitle_enable_sentence_minimum_time.value: asr_data.add_minimum_len(cfg.subtitle_sentence_minimum_time.value) + # If time offset is not zero, adjust the timestamps + if cfg.time_offset.value != 0: + for seg in asr_data.segments: + seg.start_time += cfg.time_offset.value + seg.end_time += cfg.time_offset.value + # 保存字幕文件 original_subtitle_path = Path(self.task.original_subtitle_save_path) original_subtitle_path.parent.mkdir(parents=True, exist_ok=True) asr_data.to_srt(save_path=str(original_subtitle_path)) logger.info("源字幕文件已保存到: %s", self.task.original_subtitle_save_path) - if self.task.result_subtitle_save_path: - # Make a copy to result dir as well, if exist + if self.task.type == Task.Type.TRANSCRIBE and self.task.result_subtitle_save_path: + # Make a copy to result dir as well, if this is only a transcribe task asr_data.save( save_path=self.task.result_subtitle_save_path, ass_style=self.task.subtitle_style_srt, diff --git a/app/core/thread/video_synthesis_thread.py b/app/core/thread/video_synthesis_thread.py index 4e57fcf..23991d8 100644 --- a/app/core/thread/video_synthesis_thread.py +++ b/app/core/thread/video_synthesis_thread.py @@ -37,10 +37,10 @@ def run(self): logger.info(f"时间:{datetime.datetime.now()}") self.task.status = Task.Status.SYNTHESIZING video_file = self.task.file_path - if Path(self.task.result_subtitle_save_path).is_file(): + if Path(self.task.original_subtitle_save_path).is_file(): # result sub exist (after optimizing) subtitle_file = self.task.result_subtitle_save_path - elif Path(self.task.original_subtitle_save_path).is_file(): + elif Path(self.task.result_subtitle_save_path).is_file(): # No optimzing, original sub only subtitle_file = self.task.original_subtitle_save_path else: diff --git a/app/core/utils/video_utils.py b/app/core/utils/video_utils.py index dce2d00..5331660 100644 --- a/app/core/utils/video_utils.py +++ b/app/core/utils/video_utils.py @@ -7,9 +7,10 @@ from typing import Literal from ..utils.logger import setup_logger +from PyQt5.QtCore import QObject logger = setup_logger("video_utils") - +qoVideo = QObject() # for i18n def video2audio(input_file: str, output_file: str = "", format: str = "copy") -> bool: """使用ffmpeg将视频转换为音频""" @@ -108,8 +109,8 @@ def add_subtitles( soft_subtitle: bool = False, progress_callback: callable = None ) -> None: - assert Path(input_file).is_file(), "输入文件不存在" - assert Path(subtitle_file).is_file(), "字幕文件不存在" + assert Path(input_file).is_file(), qoVideo.tr("输入文件不存在") + assert Path(subtitle_file).is_file(), qoVideo.tr("字幕文件不存在") # 移动到临时文件 Fix: 路径错误 temp_dir = Path(tempfile.gettempdir()) / "VideoCaptioner" @@ -143,9 +144,10 @@ def add_subtitles( 'ffmpeg', '-i', input_file, '-i', subtitle_file, - '-c:v', 'copy', - '-c:a', 'copy', - '-c:s', 'copy', + '-sub_charenc', 'UTF-8', + '-map', '0', + '-map', '1', + '-c', 'copy', output, '-y' ] @@ -174,8 +176,10 @@ def add_subtitles( cmd.extend(['-hwaccel', 'cuda']) cmd.extend([ '-i', input_file, + '-map', '0', # 复制所有流 '-acodec', 'copy', '-vcodec', vcodec, + '-c:s', 'copy' '-preset', quality, '-vf', vf, '-y', # 覆盖输出文件 @@ -223,10 +227,10 @@ def add_subtitles( # 计算进度百分比 if total_duration: progress = (current_time / total_duration) * 100 - progress_callback(f"{round(progress)}", "正在合成") + progress_callback(f"{round(progress)}", qoVideo.tr("正在合成")) if progress_callback: - progress_callback("100", "合成完成") + progress_callback("100", qoVideo.tr("合成完成")) # 检查进程的返回码 return_code = process.wait() if return_code != 0: diff --git a/app/view/batch_process_interface.py b/app/view/batch_process_interface.py index f8cce72..a10ebcb 100644 --- a/app/view/batch_process_interface.py +++ b/app/view/batch_process_interface.py @@ -581,7 +581,7 @@ def setup_button_layout(self): button_widget = QWidget() button_widget.setLayout(self.button_layout) - button_widget.setFixedWidth(180) + button_widget.setFixedWidth(200) self.layout.addWidget(button_widget) def mouseDoubleClickEvent(self, event): diff --git a/app/view/setting_interface.py b/app/view/setting_interface.py index f4f503e..551cb19 100644 --- a/app/view/setting_interface.py +++ b/app/view/setting_interface.py @@ -68,7 +68,7 @@ def __init__(self, parent=None): cfg.api_base, FIF.LINK, self.tr("Base URL"), - self.tr("Input OpenAI compatible Base URL \( Needs /v1 in the end. \)"), + self.tr("Input OpenAI compatible Base URL ( Needs /v1 in the end. )"), "https://api.openai.com/v1", self.llmGroup ) @@ -151,14 +151,14 @@ def __init__(self, parent=None): self.tr('修改'), FIF.FONT, self.tr('字幕样式'), - self.tr('Choose subtitle\'s style \( color, size, font ... etc.\)'), + self.tr('Choose subtitle\'s style ( color, size, font ... etc.)'), self.subtitleGroup ) self.subtitleLayoutCard = EnumComboBoxSettingCard( cfg.subtitle_layout, FIF.FONT, self.tr('字幕布局'), - self.tr('Choose subtitle\'s layout \( Show Original or Translated or both \)'), + self.tr('Choose subtitle\'s layout ( Show Original or Translated or both )'), SubtitleLayoutEnum, self.subtitleGroup ) @@ -221,7 +221,15 @@ def __init__(self, parent=None): self.tr('In milliseconds, the minimum time each sentence should at least have.'), self.subtitleGroup ) - + + self.SubtitleTimeOffsetCard = RangeSettingCard( + cfg.time_offset, + FIF.STOP_WATCH, + self.tr('Subtitle Time Offset'), + self.tr('In milliseconds, the offset to apply to all subtitle timings.'), + self.subtitleGroup + ) + # 保存配置 self.saveGroup = SettingCardGroup(self.tr("保存配置"), self.scrollWidget) self.savePathCard = PushSettingCard( @@ -360,6 +368,7 @@ def __initLayout(self): self.subtitleGroup.addSettingCard(self.saveSubtitleSuffixCard) self.subtitleGroup.addSettingCard(self.enableSubtitleSentenceMinimumTimeCard) self.subtitleGroup.addSettingCard(self.SubtitleSentenceMinimumTimeCard) + self.subtitleGroup.addSettingCard(self.SubtitleTimeOffsetCard) self.saveGroup.addSettingCard(self.savePathCard) self.personalGroup.addSettingCard(self.themeCard) diff --git a/pylist.txt b/pylist.txt deleted file mode 100644 index 491da32..0000000 --- a/pylist.txt +++ /dev/null @@ -1,22 +0,0 @@ -.\app\components\DonateDialog.py -.\app\components\FasterWhisperSettingDialog.py -.\app\components\MySettingCard.py -.\app\components\SubtitleSettingDialog.py -.\app\components\WhisperAPISettingDialog.py -.\app\components\WhisperSettingDialog.py -.\app\core\thread\create_task_thread.py -.\app\core\thread\download_thread.py -.\app\core\thread\subtitle_optimization_thread.py -.\app\core\thread\subtitle_pipeline_thread.py -.\app\core\thread\transcript_thread.py -.\app\core\thread\video_synthesis_thread.py -.\app\view\batch_process_interface.py -.\app\view\home_interface.py -.\app\view\log_window.py -.\app\view\main_window.py -.\app\view\setting_interface.py -.\app\view\subtitle_optimization_interface.py -.\app\view\subtitle_style_interface.py -.\app\view\task_creation_interface.py -.\app\view\transcription_interface.py -.\app\view\video_synthesis_interface.py diff --git a/requirements.txt b/requirements.txt index 5f358da..069bdf0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,13 @@ requests + openai + retry + PyQt5 + PyQt-Fluent-Widgets + yt_dlp + modelscope diff --git a/resource/translations/VideoCaptioner_en_US.qm b/resource/translations/VideoCaptioner_en_US.qm index 102f3b9..59ed81d 100644 Binary files a/resource/translations/VideoCaptioner_en_US.qm and b/resource/translations/VideoCaptioner_en_US.qm differ diff --git a/resource/translations/VideoCaptioner_en_US.ts b/resource/translations/VideoCaptioner_en_US.ts index d3ef1b7..a89d8bd 100644 --- a/resource/translations/VideoCaptioner_en_US.ts +++ b/resource/translations/VideoCaptioner_en_US.ts @@ -905,185 +905,195 @@ - + + Subtitle Time Offset + + + + + In milliseconds, the offset to apply to all subtitle timings. + + + + 保存配置 Save Settings - + 工作文件夹 Working Folder - + 工作目录路径 Working directory path - + 个性化 Personalization - + 应用主题 Application Theme - + 更改应用程序的外观 Change application appearance - + 浅色 Light - + 深色 Dark - - + + 使用系统设置 Use System Settings - + 主题颜色 Theme Color - + 更改应用程序的主题颜色 Change application theme color - + 界面缩放 Interface Scaling - + 更改小部件和字体的大小 Change size of widgets and fonts - + 语言 Language - + 设置您偏好的界面语言 Set your preferred interface language - - + + 关于 About - + 打开帮助页面 Open Help Page - + 帮助 Help - - + + 提供反馈 Provide Feedback - + 检查更新 Check for Updates - + 版权所有 Copyright - + 版本 Version - - + + 错误 Error - + 请先选择Whisper转录模型 Please choose Whisper transcribe model - + 请输入正确的 API Base, 含有 /v1 Please enter a valid API Base containing /v1 - + 正在检查... Checking... - - + + 检查连接 Check Connection - - + + LLM 连接测试错误 LLM connection test error - + 获取模型列表成功: Successfully fetched model list: - + 一共 Totally - + 个模型 models - + LLM 连接测试成功 LLM connection test successful - + 更新成功 Update Successful - + 配置将在重启后生效 Configuration will take effect after restart - + 选择文件夹 Select Folder @@ -1218,12 +1228,12 @@ - + 发现新功能并了解有关VideoCaptioner的使用技巧 Discover new features and learn tips about using VideoCaptioner - + 提供反馈帮助我们改进VideoCaptioner Provide feedback to help us improve VideoCaptioner @@ -1475,37 +1485,37 @@ Detail settings are in the 'Settings' on the lower left corner.Start Optimizing Subtitle... - + 字幕断句... Subtitle Sentence Breaking... - + 总结字幕... Subtitle Summerizing... - + 优化+翻译... Optimizing + Translating... - + 批量翻译单句字幕... Batch translating subtitles by single sentence... - + 优化/翻译完成 Optimizing / Translation finished - + 优化失败 Optimization failed - + {0}% 处理字幕 {0}% processing subtitle @@ -2544,12 +2554,13 @@ Detail settings are in the 'Settings' on the lower left corner. VideoSynthesisThread - + + 合成完成 Synthesis Done - + 正在合成 Synthesizing @@ -2559,7 +2570,7 @@ Detail settings are in the 'Settings' on the lower left corner. - + 视频合成失败 Video synthesizing failed @@ -2748,17 +2759,17 @@ Detail settings are in the 'Settings' on the lower left corner. qoCreateTask - + 【翻译字幕】 TranslatedSub_ - + 【修正字幕】 FixedSub_ - + 【字幕】 sub_ @@ -2912,4 +2923,27 @@ Detail settings are in the 'Settings' on the lower left corner. + + qoVideo + + + 输入文件不存在 + Input File Does Not Exist + + + + 字幕文件不存在 + Subtitle file does not exist + + + + 正在合成 + Synthesizing + + + + 合成完成 + Synthesis Done + + diff --git a/resource/translations/VideoCaptioner_zh_CN.qm b/resource/translations/VideoCaptioner_zh_CN.qm index 45dfe96..14a3047 100644 Binary files a/resource/translations/VideoCaptioner_zh_CN.qm and b/resource/translations/VideoCaptioner_zh_CN.qm differ diff --git a/resource/translations/VideoCaptioner_zh_CN.ts b/resource/translations/VideoCaptioner_zh_CN.ts index 10b248b..6b6ae13 100644 --- a/resource/translations/VideoCaptioner_zh_CN.ts +++ b/resource/translations/VideoCaptioner_zh_CN.ts @@ -218,7 +218,7 @@ Choose - 选择 + 选择 @@ -850,8 +850,8 @@ - - + + 检查连接 @@ -1041,188 +1041,198 @@ 以千分之一秒为单位,每一句的显示时间最少为。 - + + Subtitle Time Offset + 字幕时间轴调整 + + + + In milliseconds, the offset to apply to all subtitle timings. + 以千分之一秒为单位,调整整个字幕的时间轴 + + + 保存配置 - + 工作文件夹 - + 工作目录路径 - + 个性化 - + 应用主题 - + 更改应用程序的外观 - + 浅色 - + 深色 - - + + 使用系统设置 - + 主题颜色 - + 更改应用程序的主题颜色 - + 界面缩放 - + 更改小部件和字体的大小 - + 语言 - + 设置您偏好的界面语言 - - + + 关于 - + 打开帮助页面 - + 帮助 - + 发现新功能并了解有关VideoCaptioner的使用技巧 - - + + 提供反馈 - + 提供反馈帮助我们改进VideoCaptioner - + 检查更新 - + 版权所有 - + 版本 - - + + 错误 - + 请先选择Whisper转录模型 - + 更新成功 - + 配置将在重启后生效 - + 选择文件夹 - + 请输入正确的 API Base, 含有 /v1 - + 正在检查... - - + + LLM 连接测试错误 - + 获取模型列表成功: - + 一共 - + 个模型 - + LLM 连接测试成功 @@ -1473,37 +1483,37 @@ - + 字幕断句... - + 总结字幕... - + 优化+翻译... - + 批量翻译单句字幕... - + 优化/翻译完成 - + 优化失败 - + {0}% 处理字幕 @@ -2542,12 +2552,13 @@ VideoSynthesisThread - + + 合成完成 - + 正在合成 @@ -2557,7 +2568,7 @@ 等待视频合成 - + 视频合成失败 @@ -2746,17 +2757,17 @@ qoCreateTask - + 【翻译字幕】 - + 【修正字幕】 - + 【字幕】 @@ -2910,4 +2921,27 @@ 转录 + + qoVideo + + + 输入文件不存在 + + + + + 字幕文件不存在 + + + + + 正在合成 + + + + + 合成完成 + + +