9 Commits
4.2.0 ... 4.3.0

11 changed files with 314 additions and 132 deletions

View File

@@ -2,7 +2,6 @@
<img src="https://user-images.githubusercontent.com/21986859/81626588-ae5ab800-93eb-11ea-918f-ebe98c2de40a.png"/> <img src="https://user-images.githubusercontent.com/21986859/81626588-ae5ab800-93eb-11ea-918f-ebe98c2de40a.png"/>
</p> </p>
![Master Branch Version](https://img.shields.io/badge/master-v4.0.0-9cf?style=flat-square)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/k4yt3x/video2x?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/k4yt3x/video2x?style=flat-square)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/k4yt3x/video2x/Video2X%20Nightly%20Build?label=Nightly&style=flat-square) ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/k4yt3x/video2x/Video2X%20Nightly%20Build?label=Nightly&style=flat-square)
![GitHub All Releases](https://img.shields.io/github/downloads/k4yt3x/video2x/total?style=flat-square) ![GitHub All Releases](https://img.shields.io/github/downloads/k4yt3x/video2x/total?style=flat-square)

View File

@@ -5,8 +5,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: 2020-05-17 10:10-0400\n" "POT-Creation-Date: 2020-05-22 17:51-0400\n"
"PO-Revision-Date: 2020-05-17 10:13-0400\n" "PO-Revision-Date: 2020-05-22 17:54-0400\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: zh_CN\n" "Language: zh_CN\n"
@@ -21,199 +21,223 @@ msgstr ""
msgid "Upscaling Progress" msgid "Upscaling Progress"
msgstr "放大进度" msgstr "放大进度"
#: upscaler.py:109 #: upscaler.py:110
msgid "Specified or default cache directory is a file/link" msgid "Specified or default cache directory is a file/link"
msgstr "指定或默认的缓存目录是文件/链接" msgstr "指定或默认的缓存目录是文件/链接"
#: upscaler.py:115 #: upscaler.py:116
msgid "Creating cache directory {}" msgid "Creating cache directory {}"
msgstr "创建缓存目录 {}" msgstr "创建缓存目录 {}"
#: upscaler.py:118 #: upscaler.py:119
msgid "Unable to create {}" msgid "Unable to create {}"
msgstr "无法创建 {}" msgstr "无法创建 {}"
#: upscaler.py:123 #: upscaler.py:124
msgid "Extracted frames are being saved to: {}" msgid "Extracted frames are being saved to: {}"
msgstr "提取的帧将被保存到:{}" msgstr "提取的帧将被保存到:{}"
#: upscaler.py:125 #: upscaler.py:126
msgid "Upscaled frames are being saved to: {}" msgid "Upscaled frames are being saved to: {}"
msgstr "已放大的帧将被保存到:{}" msgstr "已放大的帧将被保存到:{}"
#: upscaler.py:135 #: upscaler.py:136
msgid "Cleaning up cache directory: {}" msgid "Cleaning up cache directory: {}"
msgstr "清理缓存目录:{}" msgstr "清理缓存目录:{}"
#: upscaler.py:138 #: upscaler.py:141
msgid "Unable to delete: {}" msgid "Unable to delete: {}"
msgstr "无法删除:{}" msgstr "无法删除:{}"
#: upscaler.py:144 upscaler.py:159 upscaler.py:170 #: upscaler.py:147 upscaler.py:162 upscaler.py:173
msgid "Input and output path type mismatch" msgid "Input and output path type mismatch"
msgstr "输入和输出路径类型不匹配" msgstr "输入和输出路径类型不匹配"
#: upscaler.py:145 #: upscaler.py:148
msgid "Input is multiple files but output is not directory" msgid "Input is multiple files but output is not directory"
msgstr "输入是多个文件,但输出不是目录" msgstr "输入是多个文件,但输出不是目录"
#: upscaler.py:149 #: upscaler.py:152
msgid "Input path {} is neither a file nor a directory" msgid "Input path {} is neither a file nor a directory"
msgstr "输入路径 {} 既不是文件也不是目录" msgstr "输入路径 {} 既不是文件也不是目录"
#: upscaler.py:153 upscaler.py:175 #: upscaler.py:156 upscaler.py:178
msgid "Input directory and output directory cannot be the same" msgid "Input directory and output directory cannot be the same"
msgstr "输入目录和输出目录不能相同" msgstr "输入目录和输出目录不能相同"
#: upscaler.py:160 #: upscaler.py:163
msgid "Input is single file but output is directory" msgid "Input is single file but output is directory"
msgstr "所选的输入路径是单个文件,但输出路径是目录" msgstr "所选的输入路径是单个文件,但输出路径是目录"
#: upscaler.py:163 #: upscaler.py:166
msgid "No suffix found in output file path" msgid "No suffix found in output file path"
msgstr "在输出文件路径中未找到后缀" msgstr "在输出文件路径中未找到后缀"
#: upscaler.py:164 #: upscaler.py:167
msgid "Suffix must be specified" msgid "Suffix must be specified"
msgstr "必须指定文件后缀" msgstr "必须指定文件后缀"
#: upscaler.py:171 #: upscaler.py:174
msgid "Input is directory but output is existing single file" msgid "Input is directory but output is existing single file"
msgstr "输入是目录,但输出是现有的单个文件" msgstr "输入是目录,但输出是现有的单个文件"
#: upscaler.py:180 #: upscaler.py:183
msgid "Input path is neither a file nor a directory" msgid "Input path is neither a file nor a directory"
msgstr "输入路径既不是文件也不是目录" msgstr "输入路径既不是文件也不是目录"
#: upscaler.py:189 #: upscaler.py:192
msgid "FFmpeg or FFprobe cannot be found under the specified path" msgid "FFmpeg or FFprobe cannot be found under the specified path"
msgstr "在指定的路径下找不到 FFmpeg 或 FFprobe" msgstr "在指定的路径下找不到 FFmpeg 或 FFprobe"
#: upscaler.py:190 upscaler.py:200 #: upscaler.py:193 upscaler.py:203
msgid "Please check the configuration file settings" msgid "Please check the configuration file settings"
msgstr "请检查配置文件设置" msgstr "请检查配置文件设置"
#: upscaler.py:199 #: upscaler.py:202
msgid "Specified driver executable directory doesn't exist" msgid "Specified driver executable directory doesn't exist"
msgstr "指定驱动的可执行文件不存在" msgstr "指定驱动的可执行文件不存在"
#: upscaler.py:226 #: upscaler.py:229
msgid "Failed to parse driver argument: {}" msgid "Failed to parse driver argument: {}"
msgstr "解析驱动程序参数失败:{}" msgstr "解析驱动程序参数失败:{}"
#: upscaler.py:258 #: upscaler.py:261
msgid "Unrecognized driver: {}" msgid "Unrecognized driver: {}"
msgstr "无法识别的驱动名称:{}" msgstr "无法识别的驱动名称:{}"
#: upscaler.py:298 #: upscaler.py:301
msgid "Starting progress monitor" msgid "Starting progress monitor"
msgstr "启动进度监视器" msgstr "启动进度监视器"
#: upscaler.py:303 #: upscaler.py:306
msgid "Starting upscaled image cleaner" msgid "Starting upscaled image cleaner"
msgstr "启动已放大图像清理程序" msgstr "启动已放大图像清理程序"
#: upscaler.py:312 upscaler.py:329 #: upscaler.py:315 upscaler.py:332
msgid "Killing progress monitor" msgid "Killing progress monitor"
msgstr "终结进度监视器" msgstr "终结进度监视器"
#: upscaler.py:315 upscaler.py:332 #: upscaler.py:318 upscaler.py:335
msgid "Killing upscaled image cleaner" msgid "Killing upscaled image cleaner"
msgstr "终结已放大图像清理程序" msgstr "终结已放大图像清理程序"
#: upscaler.py:336 #: upscaler.py:339
msgid "Terminating all processes" msgid "Terminating all processes"
msgstr "正在终止所有进程" msgstr "正在终止所有进程"
#: upscaler.py:343 #: upscaler.py:346
msgid "Main process waiting for subprocesses to exit" msgid "Main process waiting for subprocesses to exit"
msgstr "主进程开始等待子进程结束" msgstr "主进程开始等待子进程结束"
#: upscaler.py:362 upscaler.py:366 #: upscaler.py:365 upscaler.py:369
msgid "Subprocess {} exited with code {}" msgid "Subprocess {} exited with code {}"
msgstr "子进程 {} 结束,返回码 {}" msgstr "子进程 {} 结束,返回码 {}"
#: upscaler.py:372 #: upscaler.py:375
msgid "Stop signal received" msgid "Stop signal received"
msgstr "收到停止信号" msgstr "收到停止信号"
#: upscaler.py:377 #: upscaler.py:380
msgid "Subprocess execution ran into an error" msgid "Subprocess execution ran into an error"
msgstr "子进程执行遇到错误" msgstr "子进程执行遇到错误"
#: upscaler.py:425 #: upscaler.py:410
msgid "Upscaling single file: {}" msgid "Loading files into processing queue"
msgstr "放大单个文件:{}" msgstr "正在将文件添加到处理队列中"
#: upscaler.py:461 #: upscaler.py:415
msgid "Loading files from multiple paths"
msgstr "正在从多个路径中导入文件"
#: upscaler.py:416 upscaler.py:435 upscaler.py:442
msgid "Input path(s): {}"
msgstr "输入路径:{}"
#: upscaler.py:434
msgid "Loading single file"
msgstr "正在导入单个文件"
#: upscaler.py:441
msgid "Loading files from directory"
msgstr "正在从文件夹中导入文件"
#: upscaler.py:455
msgid "Loaded files into processing queue"
msgstr "文件已添加到处理队列"
#: upscaler.py:458
msgid "Input file: {}"
msgstr "输入文件:{}"
#: upscaler.py:485
msgid "Starting to upscale image" msgid "Starting to upscale image"
msgstr "开始放大图像" msgstr "开始放大图像"
#: upscaler.py:464 upscaler.py:526 #: upscaler.py:488 upscaler.py:550
msgid "Upscaling completed" msgid "Upscaling completed"
msgstr "放大完成" msgstr "放大完成"
#: upscaler.py:478 #: upscaler.py:502
msgid "Reading video information" msgid "Reading video information"
msgstr "读取视频信息" msgstr "读取视频信息"
#: upscaler.py:492 #: upscaler.py:516
msgid "Aborting: No video stream found" msgid "Aborting: No video stream found"
msgstr "程序中止:文件中未找到视频流" msgstr "程序中止:文件中未找到视频流"
#: upscaler.py:497 #: upscaler.py:521
msgid "Framerate: {}" msgid "Framerate: {}"
msgstr "帧率:{}" msgstr "帧率:{}"
#: upscaler.py:514 #: upscaler.py:538
msgid "Unsupported pixel format: {}" msgid "Unsupported pixel format: {}"
msgstr "不支持的像素格式:{}" msgstr "不支持的像素格式:{}"
#: upscaler.py:524 #: upscaler.py:548
msgid "Starting to upscale extracted frames" msgid "Starting to upscale extracted frames"
msgstr "开始对提取的帧进行放大" msgstr "开始对提取的帧进行放大"
#: upscaler.py:531 #: upscaler.py:555
msgid "File {} ({}) neither an image of a video" msgid "File {} ({}) neither an image nor a video"
msgstr "文件 {} {} 既不是图片也不是视频" msgstr "文件 {} {} 既不是图片也不是视频"
#: upscaler.py:532 #: upscaler.py:556
msgid "Skipping this file" msgid "Skipping this file"
msgstr "将跳过此文件" msgstr "将跳过此文件"
#: upscaler.py:542 #: upscaler.py:566
msgid "Converting extracted frames into GIF image" msgid "Converting extracted frames into GIF image"
msgstr "正在将提取的帧转换为 GIF" msgstr "正在将提取的帧转换为 GIF"
#: upscaler.py:546 upscaler.py:555 #: upscaler.py:570 upscaler.py:579
msgid "Conversion completed" msgid "Conversion completed"
msgstr "转换已完成" msgstr "转换已完成"
#: upscaler.py:551 #: upscaler.py:575
msgid "Converting extracted frames into video" msgid "Converting extracted frames into video"
msgstr "正在将提取的帧转换为视频" msgstr "正在将提取的帧转换为视频"
#: upscaler.py:559 #: upscaler.py:583
msgid "Migrating audio, subtitles and other streams to upscaled video" msgid "Migrating audio, subtitles and other streams to upscaled video"
msgstr "正在将音频、字幕和其他流迁移到放大后的视频" msgstr "正在将音频、字幕和其他流迁移到放大后的视频"
#: upscaler.py:569 #: upscaler.py:593
msgid "Failed to migrate streams" msgid "Failed to migrate streams"
msgstr "迁移流失败" msgstr "迁移流失败"
#: upscaler.py:570 #: upscaler.py:594
msgid "Trying to output video without additional streams" msgid "Trying to output video without additional streams"
msgstr "正在尝试输出不含其他流的视频" msgstr "正在尝试输出不含其他流的视频"
#: upscaler.py:586 #: upscaler.py:610
msgid "Output video file exists" msgid "Output video file exists"
msgstr "输出目标文件已存在" msgstr "输出目标文件已存在"
#: upscaler.py:590 #: upscaler.py:614
msgid "Created temporary directory to contain file" msgid "Created temporary directory to contain file"
msgstr "为文件创建了临时目录" msgstr "为文件创建了临时目录"
#: upscaler.py:593 #: upscaler.py:617
msgid "Writing intermediate file to: {}" msgid "Writing intermediate file to: {}"
msgstr "正在将中间视频文件写入至:{}" msgstr "正在将中间视频文件写入至:{}"
@@ -321,6 +345,3 @@ msgstr "发生了异常"
#~ msgid "You must specify both width and height" #~ msgid "You must specify both width and height"
#~ msgstr "您必须同时指定宽度和高度" #~ msgstr "您必须同时指定宽度和高度"
#~ msgid "Upscaling videos in directory: {}"
#~ msgstr "放大该文件夹中的所有视频:{}"

View File

@@ -4,7 +4,7 @@
Name: Video2X Upscaler Name: Video2X Upscaler
Author: K4YT3X Author: K4YT3X
Date Created: December 10, 2018 Date Created: December 10, 2018
Last Modified: May 17, 2020 Last Modified: May 22, 2020
Description: This file contains the Upscaler class. Each Description: This file contains the Upscaler class. Each
instance of the Upscaler class is an upscaler on an image or instance of the Upscaler class is an upscaler on an image or
@@ -25,6 +25,7 @@ import copy
import gettext import gettext
import importlib import importlib
import locale import locale
import mimetypes
import pathlib import pathlib
import queue import queue
import re import re
@@ -49,7 +50,7 @@ language.install()
_ = language.gettext _ = language.gettext
# version information # version information
UPSCALER_VERSION = '4.1.0' UPSCALER_VERSION = '4.1.1'
# these names are consistent for # these names are consistent for
# - driver selection in command line # - driver selection in command line
@@ -406,8 +407,14 @@ class Upscaler:
# define processing queue # define processing queue
self.processing_queue = queue.Queue() self.processing_queue = queue.Queue()
Avalon.info(_('Loading files into processing queue'))
# if input is a list of files # if input is a list of files
if isinstance(self.input, list): if isinstance(self.input, list):
Avalon.info(_('Loading files from multiple paths'))
Avalon.debug_info(_('Input path(s): {}').format(self.input))
# make output directory if it doesn't exist # make output directory if it doesn't exist
self.output.mkdir(parents=True, exist_ok=True) self.output.mkdir(parents=True, exist_ok=True)
@@ -424,12 +431,15 @@ class Upscaler:
# if input specified is single file # if input specified is single file
elif self.input.is_file(): elif self.input.is_file():
Avalon.info(_('Upscaling single file: {}').format(self.input)) Avalon.info(_('Loading single file'))
Avalon.debug_info(_('Input path(s): {}').format(self.input))
self.processing_queue.put((self.input.absolute(), self.output.absolute())) self.processing_queue.put((self.input.absolute(), self.output.absolute()))
# if input specified is a directory # if input specified is a directory
elif self.input.is_dir(): elif self.input.is_dir():
Avalon.info(_('Loading files from directory'))
Avalon.debug_info(_('Input path(s): {}').format(self.input))
# make output directory if it doesn't exist # make output directory if it doesn't exist
self.output.mkdir(parents=True, exist_ok=True) self.output.mkdir(parents=True, exist_ok=True)
for input_path in [f for f in self.input.iterdir() if f.is_file()]: for input_path in [f for f in self.input.iterdir() if f.is_file()]:
@@ -442,6 +452,11 @@ class Upscaler:
# record file count for external calls # record file count for external calls
self.total_files = self.processing_queue.qsize() self.total_files = self.processing_queue.qsize()
Avalon.info(_('Loaded files into processing queue'))
# print all files in queue for debugging
for job in self.processing_queue.queue:
Avalon.debug_info(_('Input file: {}').format(job[0].absolute()))
try: try:
while not self.processing_queue.empty(): while not self.processing_queue.empty():
@@ -457,6 +472,13 @@ class Upscaler:
input_file_type = input_file_mime_type.split('/')[0] input_file_type = input_file_mime_type.split('/')[0]
input_file_subtype = input_file_mime_type.split('/')[1] input_file_subtype = input_file_mime_type.split('/')[1]
# in case python-magic fails to detect file type
# try guessing file mime type with mimetypes
if input_file_type not in ['image', 'video']:
input_file_mime_type = mimetypes.guess_type(self.current_input_file.name)[0]
input_file_type = input_file_mime_type.split('/')[0]
input_file_subtype = input_file_mime_type.split('/')[1]
# start handling input # start handling input
# if input file is a static image # if input file is a static image
if input_file_type == 'image' and input_file_subtype != 'gif': if input_file_type == 'image' and input_file_subtype != 'gif':
@@ -530,7 +552,7 @@ class Upscaler:
# if file is none of: image, image/gif, video # if file is none of: image, image/gif, video
# skip to the next task # skip to the next task
else: else:
Avalon.error(_('File {} ({}) neither an image of a video').format(self.current_input_file, input_file_mime_type)) Avalon.error(_('File {} ({}) neither an image nor a video').format(self.current_input_file, input_file_mime_type))
Avalon.warning(_('Skipping this file')) Avalon.warning(_('Skipping this file'))
self.processing_queue.task_done() self.processing_queue.task_done()
self.total_processed += 1 self.total_processed += 1

View File

@@ -5,7 +5,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2020-05-17 10:10-0400\n" "POT-Creation-Date: 2020-05-22 17:51-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -19,199 +19,223 @@ msgstr ""
msgid "Upscaling Progress" msgid "Upscaling Progress"
msgstr "" msgstr ""
#: upscaler.py:109 #: upscaler.py:110
msgid "Specified or default cache directory is a file/link" msgid "Specified or default cache directory is a file/link"
msgstr "" msgstr ""
#: upscaler.py:115 #: upscaler.py:116
msgid "Creating cache directory {}" msgid "Creating cache directory {}"
msgstr "" msgstr ""
#: upscaler.py:118 #: upscaler.py:119
msgid "Unable to create {}" msgid "Unable to create {}"
msgstr "" msgstr ""
#: upscaler.py:123 #: upscaler.py:124
msgid "Extracted frames are being saved to: {}" msgid "Extracted frames are being saved to: {}"
msgstr "" msgstr ""
#: upscaler.py:125 #: upscaler.py:126
msgid "Upscaled frames are being saved to: {}" msgid "Upscaled frames are being saved to: {}"
msgstr "" msgstr ""
#: upscaler.py:135 #: upscaler.py:136
msgid "Cleaning up cache directory: {}" msgid "Cleaning up cache directory: {}"
msgstr "" msgstr ""
#: upscaler.py:138 #: upscaler.py:141
msgid "Unable to delete: {}" msgid "Unable to delete: {}"
msgstr "" msgstr ""
#: upscaler.py:144 upscaler.py:159 upscaler.py:170 #: upscaler.py:147 upscaler.py:162 upscaler.py:173
msgid "Input and output path type mismatch" msgid "Input and output path type mismatch"
msgstr "" msgstr ""
#: upscaler.py:145 #: upscaler.py:148
msgid "Input is multiple files but output is not directory" msgid "Input is multiple files but output is not directory"
msgstr "" msgstr ""
#: upscaler.py:149 #: upscaler.py:152
msgid "Input path {} is neither a file nor a directory" msgid "Input path {} is neither a file nor a directory"
msgstr "" msgstr ""
#: upscaler.py:153 upscaler.py:175 #: upscaler.py:156 upscaler.py:178
msgid "Input directory and output directory cannot be the same" msgid "Input directory and output directory cannot be the same"
msgstr "" msgstr ""
#: upscaler.py:160 #: upscaler.py:163
msgid "Input is single file but output is directory" msgid "Input is single file but output is directory"
msgstr "" msgstr ""
#: upscaler.py:163 #: upscaler.py:166
msgid "No suffix found in output file path" msgid "No suffix found in output file path"
msgstr "" msgstr ""
#: upscaler.py:164 #: upscaler.py:167
msgid "Suffix must be specified" msgid "Suffix must be specified"
msgstr "" msgstr ""
#: upscaler.py:171 #: upscaler.py:174
msgid "Input is directory but output is existing single file" msgid "Input is directory but output is existing single file"
msgstr "" msgstr ""
#: upscaler.py:180 #: upscaler.py:183
msgid "Input path is neither a file nor a directory" msgid "Input path is neither a file nor a directory"
msgstr "" msgstr ""
#: upscaler.py:189 #: upscaler.py:192
msgid "FFmpeg or FFprobe cannot be found under the specified path" msgid "FFmpeg or FFprobe cannot be found under the specified path"
msgstr "" msgstr ""
#: upscaler.py:190 upscaler.py:200 #: upscaler.py:193 upscaler.py:203
msgid "Please check the configuration file settings" msgid "Please check the configuration file settings"
msgstr "" msgstr ""
#: upscaler.py:199 #: upscaler.py:202
msgid "Specified driver executable directory doesn't exist" msgid "Specified driver executable directory doesn't exist"
msgstr "" msgstr ""
#: upscaler.py:226 #: upscaler.py:229
msgid "Failed to parse driver argument: {}" msgid "Failed to parse driver argument: {}"
msgstr "" msgstr ""
#: upscaler.py:258 #: upscaler.py:261
msgid "Unrecognized driver: {}" msgid "Unrecognized driver: {}"
msgstr "" msgstr ""
#: upscaler.py:298 #: upscaler.py:301
msgid "Starting progress monitor" msgid "Starting progress monitor"
msgstr "" msgstr ""
#: upscaler.py:303 #: upscaler.py:306
msgid "Starting upscaled image cleaner" msgid "Starting upscaled image cleaner"
msgstr "" msgstr ""
#: upscaler.py:312 upscaler.py:329 #: upscaler.py:315 upscaler.py:332
msgid "Killing progress monitor" msgid "Killing progress monitor"
msgstr "" msgstr ""
#: upscaler.py:315 upscaler.py:332 #: upscaler.py:318 upscaler.py:335
msgid "Killing upscaled image cleaner" msgid "Killing upscaled image cleaner"
msgstr "" msgstr ""
#: upscaler.py:336 #: upscaler.py:339
msgid "Terminating all processes" msgid "Terminating all processes"
msgstr "" msgstr ""
#: upscaler.py:343 #: upscaler.py:346
msgid "Main process waiting for subprocesses to exit" msgid "Main process waiting for subprocesses to exit"
msgstr "" msgstr ""
#: upscaler.py:362 upscaler.py:366 #: upscaler.py:365 upscaler.py:369
msgid "Subprocess {} exited with code {}" msgid "Subprocess {} exited with code {}"
msgstr "" msgstr ""
#: upscaler.py:372 #: upscaler.py:375
msgid "Stop signal received" msgid "Stop signal received"
msgstr "" msgstr ""
#: upscaler.py:377 #: upscaler.py:380
msgid "Subprocess execution ran into an error" msgid "Subprocess execution ran into an error"
msgstr "" msgstr ""
#: upscaler.py:425 #: upscaler.py:410
msgid "Upscaling single file: {}" msgid "Loading files into processing queue"
msgstr "" msgstr ""
#: upscaler.py:461 #: upscaler.py:415
msgid "Loading files from multiple paths"
msgstr ""
#: upscaler.py:416 upscaler.py:435 upscaler.py:442
msgid "Input path(s): {}"
msgstr ""
#: upscaler.py:434
msgid "Loading single file"
msgstr ""
#: upscaler.py:441
msgid "Loading files from directory"
msgstr ""
#: upscaler.py:455
msgid "Loaded files into processing queue"
msgstr ""
#: upscaler.py:458
msgid "Input file: {}"
msgstr ""
#: upscaler.py:485
msgid "Starting to upscale image" msgid "Starting to upscale image"
msgstr "" msgstr ""
#: upscaler.py:464 upscaler.py:526 #: upscaler.py:488 upscaler.py:550
msgid "Upscaling completed" msgid "Upscaling completed"
msgstr "" msgstr ""
#: upscaler.py:478 #: upscaler.py:502
msgid "Reading video information" msgid "Reading video information"
msgstr "" msgstr ""
#: upscaler.py:492 #: upscaler.py:516
msgid "Aborting: No video stream found" msgid "Aborting: No video stream found"
msgstr "" msgstr ""
#: upscaler.py:497 #: upscaler.py:521
msgid "Framerate: {}" msgid "Framerate: {}"
msgstr "" msgstr ""
#: upscaler.py:514 #: upscaler.py:538
msgid "Unsupported pixel format: {}" msgid "Unsupported pixel format: {}"
msgstr "" msgstr ""
#: upscaler.py:524 #: upscaler.py:548
msgid "Starting to upscale extracted frames" msgid "Starting to upscale extracted frames"
msgstr "" msgstr ""
#: upscaler.py:531 #: upscaler.py:555
msgid "File {} ({}) neither an image of a video" msgid "File {} ({}) neither an image nor a video"
msgstr "" msgstr ""
#: upscaler.py:532 #: upscaler.py:556
msgid "Skipping this file" msgid "Skipping this file"
msgstr "" msgstr ""
#: upscaler.py:542 #: upscaler.py:566
msgid "Converting extracted frames into GIF image" msgid "Converting extracted frames into GIF image"
msgstr "" msgstr ""
#: upscaler.py:546 upscaler.py:555 #: upscaler.py:570 upscaler.py:579
msgid "Conversion completed" msgid "Conversion completed"
msgstr "" msgstr ""
#: upscaler.py:551 #: upscaler.py:575
msgid "Converting extracted frames into video" msgid "Converting extracted frames into video"
msgstr "" msgstr ""
#: upscaler.py:559 #: upscaler.py:583
msgid "Migrating audio, subtitles and other streams to upscaled video" msgid "Migrating audio, subtitles and other streams to upscaled video"
msgstr "" msgstr ""
#: upscaler.py:569 #: upscaler.py:593
msgid "Failed to migrate streams" msgid "Failed to migrate streams"
msgstr "" msgstr ""
#: upscaler.py:570 #: upscaler.py:594
msgid "Trying to output video without additional streams" msgid "Trying to output video without additional streams"
msgstr "" msgstr ""
#: upscaler.py:586 #: upscaler.py:610
msgid "Output video file exists" msgid "Output video file exists"
msgstr "" msgstr ""
#: upscaler.py:590 #: upscaler.py:614
msgid "Created temporary directory to contain file" msgid "Created temporary directory to contain file"
msgstr "" msgstr ""
#: upscaler.py:593 #: upscaler.py:617
msgid "Writing intermediate file to: {}" msgid "Writing intermediate file to: {}"
msgstr "" msgstr ""

View File

@@ -13,7 +13,7 @@ __ __ _ _ ___ __ __
Name: Video2X Controller Name: Video2X Controller
Creator: K4YT3X Creator: K4YT3X
Date Created: Feb 24, 2018 Date Created: Feb 24, 2018
Last Modified: May 15, 2020 Last Modified: May 23, 2020
Editor: BrianPetkovsek Editor: BrianPetkovsek
Last Modified: June 17, 2019 Last Modified: June 17, 2019
@@ -80,7 +80,7 @@ language.install()
_ = language.gettext _ = language.gettext
CLI_VERSION = '4.0.0' CLI_VERSION = '4.0.1'
LEGAL_INFO = _('''Video2X CLI Version: {} LEGAL_INFO = _('''Video2X CLI Version: {}
Upscaler Version: {} Upscaler Version: {}
@@ -107,8 +107,15 @@ def parse_arguments():
# video options # video options
video2x_options = parser.add_argument_group(_('Video2X Options')) video2x_options = parser.add_argument_group(_('Video2X Options'))
video2x_options.add_argument('-h', '--help', action='help', help=_('show this help message and exit')) video2x_options.add_argument('-h', '--help', action='help', help=_('show this help message and exit'))
video2x_options.add_argument('-i', '--input', type=pathlib.Path, help=_('source video file/directory'))
video2x_options.add_argument('-o', '--output', type=pathlib.Path, help=_('output video file/directory')) # if help is in arguments list
# do not require input and output path to be specified
require_input_output = True
if '-h' in sys.argv or '--help' in sys.argv:
require_input_output = False
video2x_options.add_argument('-i', '--input', type=pathlib.Path, help=_('source video file/directory'), required=require_input_output)
video2x_options.add_argument('-o', '--output', type=pathlib.Path, help=_('output video file/directory'), required=require_input_output)
video2x_options.add_argument('-c', '--config', type=pathlib.Path, help=_('video2x config file path'), action='store', video2x_options.add_argument('-c', '--config', type=pathlib.Path, help=_('video2x config file path'), action='store',
default=pathlib.Path(__file__).parent.absolute() / 'video2x.yaml') default=pathlib.Path(__file__).parent.absolute() / 'video2x.yaml')
video2x_options.add_argument('-v', '--version', help=_('display version, lawful information and exit'), action='store_true') video2x_options.add_argument('-v', '--version', help=_('display version, lawful information and exit'), action='store_true')

View File

@@ -1,7 +1,7 @@
# Name: Video2X Configuration File # Name: Video2X Configuration File
# Creator: K4YT3X # Creator: K4YT3X
# Date Created: October 23, 2018 # Date Created: October 23, 2018
# Last Modified: May 17, 2020 # Last Modified: May 23, 2020
# Values here are the default values. Change the value here to # Values here are the default values. Change the value here to
# save the default value permanently. # save the default value permanently.
# Items commented out are parameters irrelevant to this context # Items commented out are parameters irrelevant to this context
@@ -123,6 +123,7 @@ ffmpeg:
'-crf': 17 # H.264 Constant Rate Factor '-crf': 17 # H.264 Constant Rate Factor
'-b:v': null # target average bitrate '-b:v': null # target average bitrate
'-vf': 'pad=ceil(iw/2)*2:ceil(ih/2)*2' # ensure output is divisible by 2, recommended for libx264 '-vf': 'pad=ceil(iw/2)*2:ceil(ih/2)*2' # ensure output is divisible by 2, recommended for libx264
'-tune': 'animation' # encoding tuning film/animation/grain/stillimage/fastdecode/zerolatency/psnr/ssim
# Step 3: Streams Migration # Step 3: Streams Migration
# migrate audio and subtitle streams from original # migrate audio and subtitle streams from original
# video into the upscaled video # video into the upscaled video

View File

@@ -4,7 +4,7 @@
Creator: Video2X GUI Creator: Video2X GUI
Author: K4YT3X Author: K4YT3X
Date Created: May 5, 2020 Date Created: May 5, 2020
Last Modified: May 17, 2020 Last Modified: May 23, 2020
""" """
# local imports # local imports
@@ -15,6 +15,7 @@ from wrappers.ffmpeg import Ffmpeg
# built-in imports # built-in imports
import contextlib import contextlib
import json import json
import mimetypes
import os import os
import pathlib import pathlib
import sys import sys
@@ -25,12 +26,13 @@ import urllib
import yaml import yaml
# third-party imports # third-party imports
from PyQt5 import QtGui, uic from PyQt5 import uic
from PyQt5.QtCore import * from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import * from PyQt5.QtWidgets import *
import magic import magic
GUI_VERSION = '2.3.0' GUI_VERSION = '2.4.0'
LEGAL_INFO = f'''Video2X GUI Version: {GUI_VERSION}\\ LEGAL_INFO = f'''Video2X GUI Version: {GUI_VERSION}\\
Upscaler Version: {UPSCALER_VERSION}\\ Upscaler Version: {UPSCALER_VERSION}\\
@@ -131,6 +133,14 @@ class InputTableModel(QAbstractTableModel):
input_file_mime_type = magic.from_file(str(file_path.absolute()), mime=True) input_file_mime_type = magic.from_file(str(file_path.absolute()), mime=True)
input_file_type = input_file_mime_type.split('/')[0] input_file_type = input_file_mime_type.split('/')[0]
input_file_subtype = input_file_mime_type.split('/')[1] input_file_subtype = input_file_mime_type.split('/')[1]
# in case python-magic fails to detect file type
# try guessing file mime type with mimetypes
if input_file_type not in ['image', 'video']:
input_file_mime_type = mimetypes.guess_type(file_path.name)[0]
input_file_type = input_file_mime_type.split('/')[0]
input_file_subtype = input_file_mime_type.split('/')[1]
if input_file_type == 'image': if input_file_type == 'image':
if input_file_subtype == 'gif': if input_file_subtype == 'gif':
return 'GIF' return 'GIF'
@@ -182,11 +192,22 @@ class Video2XMainWindow(QMainWindow):
# set window title and icon # set window title and icon
self.video2x_icon_path = str(resource_path('images/video2x.png')) self.video2x_icon_path = str(resource_path('images/video2x.png'))
self.setWindowTitle(f'Video2X GUI {GUI_VERSION}') self.setWindowTitle(f'Video2X GUI {GUI_VERSION}')
self.setWindowIcon(QtGui.QIcon(self.video2x_icon_path)) self.setWindowIcon(QIcon(self.video2x_icon_path))
# register shortcut keys
QShortcut(QKeySequence(Qt.CTRL + Qt.Key_W), self, self.close)
QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q), self, self.close)
QShortcut(QKeySequence(Qt.CTRL + Qt.Key_I), self, self.select_input_file)
QShortcut(QKeySequence(Qt.CTRL + Qt.Key_O), self, self.select_output_file)
QShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_I), self, self.select_input_folder)
QShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_O), self, self.select_output_folder)
# menu bar # menu bar
self.action_exit = self.findChild(QAction, 'actionExit') self.action_exit = self.findChild(QAction, 'actionExit')
self.action_exit.triggered.connect(sys.exit) self.action_exit.triggered.connect(self.close)
self.action_shortcuts = self.findChild(QAction, 'actionShortcuts')
self.action_shortcuts.triggered.connect(self.show_shortcuts)
self.action_about = self.findChild(QAction, 'actionAbout') self.action_about = self.findChild(QAction, 'actionAbout')
self.action_about.triggered.connect(self.show_about) self.action_about.triggered.connect(self.show_about)
@@ -362,6 +383,7 @@ class Video2XMainWindow(QMainWindow):
self.ffmpeg_assemble_video_output_options_video_codec_line_edit = self.findChild(QLineEdit, 'ffmpegAssembleVideoOutputOptionsVideoCodecLineEdit') self.ffmpeg_assemble_video_output_options_video_codec_line_edit = self.findChild(QLineEdit, 'ffmpegAssembleVideoOutputOptionsVideoCodecLineEdit')
self.ffmpeg_assemble_video_output_options_pixel_format_line_edit = self.findChild(QLineEdit, 'ffmpegAssembleVideoOutputOptionsPixelFormatLineEdit') self.ffmpeg_assemble_video_output_options_pixel_format_line_edit = self.findChild(QLineEdit, 'ffmpegAssembleVideoOutputOptionsPixelFormatLineEdit')
self.ffmpeg_assemble_video_output_options_crf_spin_box = self.findChild(QSpinBox, 'ffmpegAssembleVideoOutputOptionsCrfSpinBox') self.ffmpeg_assemble_video_output_options_crf_spin_box = self.findChild(QSpinBox, 'ffmpegAssembleVideoOutputOptionsCrfSpinBox')
self.ffmpeg_assemble_video_output_options_tune_combo_box = self.findChild(QComboBox, 'ffmpegAssembleVideoOutputOptionsTuneComboBox')
self.ffmpeg_assemble_video_output_options_bitrate_line_edit = self.findChild(QLineEdit, 'ffmpegAssembleVideoOutputOptionsBitrateLineEdit') self.ffmpeg_assemble_video_output_options_bitrate_line_edit = self.findChild(QLineEdit, 'ffmpegAssembleVideoOutputOptionsBitrateLineEdit')
self.ffmpeg_assemble_video_output_options_ensure_divisible_check_box = self.findChild(QCheckBox, 'ffmpegAssembleVideoOutputOptionsEnsureDivisibleCheckBox') self.ffmpeg_assemble_video_output_options_ensure_divisible_check_box = self.findChild(QCheckBox, 'ffmpegAssembleVideoOutputOptionsEnsureDivisibleCheckBox')
self.ffmpeg_assemble_video_hardware_acceleration_check_box = self.findChild(QCheckBox, 'ffmpegAssembleVideoHardwareAccelerationCheckBox') self.ffmpeg_assemble_video_hardware_acceleration_check_box = self.findChild(QCheckBox, 'ffmpegAssembleVideoHardwareAccelerationCheckBox')
@@ -509,6 +531,7 @@ class Video2XMainWindow(QMainWindow):
self.ffmpeg_assemble_video_output_options_video_codec_line_edit.setText(settings['output_options']['-vcodec']) self.ffmpeg_assemble_video_output_options_video_codec_line_edit.setText(settings['output_options']['-vcodec'])
self.ffmpeg_assemble_video_output_options_pixel_format_line_edit.setText(settings['output_options']['-pix_fmt']) self.ffmpeg_assemble_video_output_options_pixel_format_line_edit.setText(settings['output_options']['-pix_fmt'])
self.ffmpeg_assemble_video_output_options_crf_spin_box.setValue(settings['output_options']['-crf']) self.ffmpeg_assemble_video_output_options_crf_spin_box.setValue(settings['output_options']['-crf'])
self.ffmpeg_assemble_video_output_options_tune_combo_box.setCurrentText(settings['output_options']['-tune'])
self.ffmpeg_assemble_video_output_options_bitrate_line_edit.setText(settings['output_options']['-b:v']) self.ffmpeg_assemble_video_output_options_bitrate_line_edit.setText(settings['output_options']['-b:v'])
# migrate streams # migrate streams
@@ -608,6 +631,7 @@ class Video2XMainWindow(QMainWindow):
self.config['ffmpeg']['assemble_video']['output_options']['-vcodec'] = self.ffmpeg_assemble_video_output_options_video_codec_line_edit.text() self.config['ffmpeg']['assemble_video']['output_options']['-vcodec'] = self.ffmpeg_assemble_video_output_options_video_codec_line_edit.text()
self.config['ffmpeg']['assemble_video']['output_options']['-pix_fmt'] = self.ffmpeg_assemble_video_output_options_pixel_format_line_edit.text() self.config['ffmpeg']['assemble_video']['output_options']['-pix_fmt'] = self.ffmpeg_assemble_video_output_options_pixel_format_line_edit.text()
self.config['ffmpeg']['assemble_video']['output_options']['-crf'] = self.ffmpeg_assemble_video_output_options_crf_spin_box.value() self.config['ffmpeg']['assemble_video']['output_options']['-crf'] = self.ffmpeg_assemble_video_output_options_crf_spin_box.value()
self.config['ffmpeg']['assemble_video']['output_options']['-tune'] = self.ffmpeg_assemble_video_output_options_tune_combo_box.currentText()
if self.ffmpeg_assemble_video_output_options_bitrate_line_edit.text() != '': if self.ffmpeg_assemble_video_output_options_bitrate_line_edit.text() != '':
self.config['ffmpeg']['assemble_video']['output_options']['-b:v'] = self.ffmpeg_assemble_video_output_options_bitrate_line_edit.text() self.config['ffmpeg']['assemble_video']['output_options']['-b:v'] = self.ffmpeg_assemble_video_output_options_bitrate_line_edit.text()
else: else:
@@ -838,6 +862,13 @@ class Video2XMainWindow(QMainWindow):
input_file_type = input_file_mime_type.split('/')[0] input_file_type = input_file_mime_type.split('/')[0]
input_file_subtype = input_file_mime_type.split('/')[1] input_file_subtype = input_file_mime_type.split('/')[1]
# in case python-magic fails to detect file type
# try guessing file mime type with mimetypes
if input_file_type not in ['image', 'video']:
input_file_mime_type = mimetypes.guess_type(input_path.name)[0]
input_file_type = input_file_mime_type.split('/')[0]
input_file_subtype = input_file_mime_type.split('/')[1]
# if input file is an image # if input file is an image
if input_file_type == 'image': if input_file_type == 'image':
@@ -917,10 +948,23 @@ class Video2XMainWindow(QMainWindow):
return return
driver_line_edit.setText(str(driver_binary_path.absolute())) driver_line_edit.setText(str(driver_binary_path.absolute()))
def show_about(self, message: str): def show_shortcuts(self):
message_box = QMessageBox(self)
message_box.setWindowTitle('Video2X Shortcuts')
message_box.setTextFormat(Qt.MarkdownText)
shortcut_information = '''**Ctrl+W**:\tExit application\\
**Ctrl+Q**:\tExit application\\
**Ctrl+I**:\tOpen select input file dialog\\
**Ctrl+O**:\tOpen select output file dialog\\
**Ctrl+Shift+I**:\tOpen select input folder dialog\\
**Ctrl+Shift+O**:\tOpen select output folder dialog'''
message_box.setText(shortcut_information)
message_box.exec_()
def show_about(self):
message_box = QMessageBox(self) message_box = QMessageBox(self)
message_box.setWindowTitle('About Video2X') message_box.setWindowTitle('About Video2X')
message_box.setIconPixmap(QtGui.QPixmap(self.video2x_icon_path).scaled(64, 64)) message_box.setIconPixmap(QPixmap(self.video2x_icon_path).scaled(64, 64))
message_box.setTextFormat(Qt.MarkdownText) message_box.setTextFormat(Qt.MarkdownText)
message_box.setText(LEGAL_INFO) message_box.setText(LEGAL_INFO)
message_box.exec_() message_box.exec_()
@@ -947,7 +991,7 @@ class Video2XMainWindow(QMainWindow):
message_box.setTextFormat(Qt.MarkdownText) message_box.setTextFormat(Qt.MarkdownText)
error_message = '''Upscaler ran into an error:\\ error_message = '''Upscaler ran into an error:\\
**{}**\\ {}\\
Check the console output for details.\\ Check the console output for details.\\
When reporting an error, please include console output.\\ When reporting an error, please include console output.\\
You can [submit an issue on GitHub](https://github.com/k4yt3x/video2x/issues/new?assignees=K4YT3X&labels=bug&template=bug-report.md&title={}) to report this error.''' You can [submit an issue on GitHub](https://github.com/k4yt3x/video2x/issues/new?assignees=K4YT3X&labels=bug&template=bug-report.md&title={}) to report this error.'''
@@ -1008,7 +1052,7 @@ You can [submit an issue on GitHub](https://github.com/k4yt3x/video2x/issues/new
# if show frame is checked, show preview image # if show frame is checked, show preview image
if self.frame_preview_show_preview_check_box.isChecked() and last_frame_upscaled.is_file(): if self.frame_preview_show_preview_check_box.isChecked() and last_frame_upscaled.is_file():
last_frame_pixmap = QtGui.QPixmap(str(last_frame_upscaled.absolute())) last_frame_pixmap = QPixmap(str(last_frame_upscaled.absolute()))
# the -2 here behind geometry subtracts frame size from width and height # the -2 here behind geometry subtracts frame size from width and height
self.frame_preview_label.setPixmap(last_frame_pixmap.scaled(self.frame_preview_label.width() - 2, self.frame_preview_label.setPixmap(last_frame_pixmap.scaled(self.frame_preview_label.width() - 2,
self.frame_preview_label.height() - 2, self.frame_preview_label.height() - 2,
@@ -1152,7 +1196,7 @@ You can [submit an issue on GitHub](https://github.com/k4yt3x/video2x/issues/new
except AttributeError: except AttributeError:
return True return True
def closeEvent(self, event: QtGui.QCloseEvent): def closeEvent(self, event: QCloseEvent):
# try cleaning up temp directories # try cleaning up temp directories
if self.stop(): if self.stop():
event.accept() event.accept()

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject> <!DOCTYPE QtCreatorProject>
<!-- Written by QtCreator 4.12.0, 2020-05-17T16:15:14. --> <!-- Written by QtCreator 4.12.0, 2020-05-23T08:51:47. -->
<qtcreator> <qtcreator>
<data> <data>
<variable>EnvironmentId</variable> <variable>EnvironmentId</variable>

View File

@@ -1707,7 +1707,7 @@
<item> <item>
<widget class="QTabWidget" name="ffmpegSettingsTabWidget"> <widget class="QTabWidget" name="ffmpegSettingsTabWidget">
<property name="currentIndex"> <property name="currentIndex">
<number>0</number> <number>2</number>
</property> </property>
<widget class="QWidget" name="ffmpegGlobalOptionsTab"> <widget class="QWidget" name="ffmpegGlobalOptionsTab">
<attribute name="title"> <attribute name="title">
@@ -1929,7 +1929,7 @@
</sizepolicy> </sizepolicy>
</property> </property>
<property name="text"> <property name="text">
<string>yuv444p10le</string> <string>yuv420p</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -1956,6 +1956,64 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<layout class="QHBoxLayout" name="ffmpegAssembleVideoOutputOptionsTuneHorizontalLayout">
<item>
<widget class="QLabel" name="ffmpegAssembleVideoOutputOptionsTuneLabel">
<property name="text">
<string>H.264/H.265 Tune (-tune)</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="ffmpegAssembleVideoOutputOptionsTuneComboBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;- film (x264 only): use for high quality movie content; lowers deblocking&lt;/p&gt;&lt;p&gt;- animation (x264 only): good for cartoons; uses higher deblocking and more reference frames&lt;/p&gt;&lt;p&gt;- grain: preserves the grain structure in old, grainy film material&lt;/p&gt;&lt;p&gt;- stillimage: (x264 only) good for slideshow-like content&lt;/p&gt;&lt;p&gt;- fastdecode: allows faster decoding by disabling certain filters&lt;/p&gt;&lt;p&gt;- zerolatency: good for fast encoding and low-latency streaming&lt;/p&gt;&lt;p&gt;- psnr: ignore this as it is only used for codec development&lt;/p&gt;&lt;p&gt;- ssim: ignore this as it is only used for codec development &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<item>
<property name="text">
<string>animation</string>
</property>
</item>
<item>
<property name="text">
<string>film</string>
</property>
</item>
<item>
<property name="text">
<string>grain</string>
</property>
</item>
<item>
<property name="text">
<string>stillimage</string>
</property>
</item>
<item>
<property name="text">
<string>fastdecode</string>
</property>
</item>
<item>
<property name="text">
<string>zerolatency</string>
</property>
</item>
<item>
<property name="text">
<string>psnr</string>
</property>
</item>
<item>
<property name="text">
<string>ssim</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item> <item>
<layout class="QHBoxLayout" name="ffmpegAssembleVideoOutputOptionsBitrateHorizontalLayout"> <layout class="QHBoxLayout" name="ffmpegAssembleVideoOutputOptionsBitrateHorizontalLayout">
<item> <item>
@@ -2596,6 +2654,7 @@
<property name="title"> <property name="title">
<string>Help</string> <string>Help</string>
</property> </property>
<addaction name="actionShortcuts"/>
<addaction name="actionAbout"/> <addaction name="actionAbout"/>
</widget> </widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
@@ -2612,6 +2671,11 @@
<string>About</string> <string>About</string>
</property> </property>
</action> </action>
<action name="actionShortcuts">
<property name="text">
<string>Shortcuts</string>
</property>
</action>
</widget> </widget>
<resources/> <resources/>
<connections/> <connections/>