NAZOrip 批量封装工具 说明 & 下载
- 下载地址位于文末 *
一、前言 & 功能简介
由于我组封装用量不大(通常不会在同一视频下封装多条音轨、字幕轨道),但对封装要求较高(需要指明轨道语言、轨道作者等),而通常封装面对十几个或几十个文件。针对我组情况,开发了这套支持对批量序列文件封装、支持文件名格式化输出的工具。
封装工具功能单纯,做了图形界面,过程一如既往的繁琐。
二、使用说明
如上图所示,本工具基于mkvtoolnix,系mkvmerge的图形界面(GUI)。如有需求,使用前请先到官方地址下载mkvtoolnix,并在GUI中选择安装目录。文件夹内虽然已经附带mkvmerge,确保用户未安装mkvtools时也能正常使用功能,但由于本界面以后大概率不会更新,不保证内核最新。
简单使用步骤:
- (可选)安装MKVToolNix。
- 下载、解压、运行软件。
- 将待封装的成序列文件,分别置于同一文件夹。
(* 例如:要封装12话动画,需将12个视频流放置于同一文件夹,音频流同理。视频与音频不必放在同一文件夹) - 在GUI中选择起止话数,例如5到12话。
- 将你选择的第一话(如上一例中的第五话)的视频、音频、字幕、章节文件输入指定位置,并指定各轨道名称。
- 点击开始封装,并等待封装完成。
三、部分实现逻辑(进阶向)
总的来说,界面能实现的逻辑分为两种模式,分别是图形界面(GUI)模式、以及命令行模式。
下面首先介绍GUI模式。
1、图形界面模式下,对每个类型的文件都支持自动识别序列功能。作为对之前批量压制工具中勉强堪用的识别功能的补完,这次重新设计了算法,让它比原先来的更智能一些。虽然仍然只支持包含阿拉伯数字的序列(不支持中文),但简单来说这个识别工具现在能做到如下操作了:
待识别序列:
识别结果:
简单来说,目前的序列识别支持相似文件名检索、支持UTF-8编码、并且具有排除同文件夹下的无关文件、以及排除相似文件的文件名中其他数字干扰的能力。
2、UI模式下各轨道支持输入的文件格式如下:
视频轨:hevc\mp4\mkv
音频轨:m4a\aac\ac3\mp3\flac\mp4\mkv
字幕轨:ass\srt\sup\mkv
章节轨:xml\mkv
可见,各轨道分别支持输入裸流、或包含流的封装好的文件。唯一区别是,在输入诸如hevc、m4a等流文件时不需要指定轨道数,而在输入mkv、mp4等封装格式时,则需要指明选择第几轨音频、第几轨字幕。UI模式下默认只能封装一轨音频和字幕,默认视频流和音频流的语言是jpn,而字幕的语言是chi,这是根据我组需求制定的。
制定轨道数时,需要注意选择的数字系该文件的第X轨,而不是封装后的第X轨,且应注意数字序号从0开始。
例如,如果某个mkv中封装了10种语言的音轨,我们希望提取其正数第一条日文音轨,则应载入后选择序号0。
3、关于批量格式化输出功能的解释:
UI模式下支持对每个输出文件分别指定自己的文件名,并且支持文件名格式化。
在“文件名格式”的选项中,有一个必选参数:{number},以及一个可选参数:{name}。在封装时,UI会自动将两个参数分别解释为:当前话的集数,以及当前集的标题。
需要注意的是,如果要使用{name}参数,则需要预先配置好一个txt文件,例如如果要输出12个文件,则txt中应包含12行(空行会自动被省略),每行对应一话的标题,如果数目出错会收到警告。
例如:如果要格式化输出上文图中所示文件序列的例子,使用的“文件名格式”参数应为:
[Encoder05] 冴えない彼女の育てかた♭ #{number} {name} (BDRip.HEVC.1080P.YUV420P10.QAAC)
同时配置文件内容为:
恋と純情のサービス回
冴えない竜虎の相見えかた
本気で本当な分岐点
初稿と二稿と大長考
二泊三日の新ルート
締め切りが先か、覚醒が先か
雪に埋もれたマスターアップ
リベンジまみれの新企画
フラグを折らなかった彼女
卒業式と超展開
そして竜虎は神に挑まん
再起と新規のゲームスタート
接着介绍高级模式(命令行模式)。
基于我组需求,制作了功能简单的GUI,但这也限制了轨道数以及轨道语言,故最后开放了一个命令行模式接口,以满足普遍性需求。
具体逻辑为,面对复杂封装时,首先将第一话在MKVToolNix GUI中完成封装,并选择混流->显示命令行调取shell命令。将其粘贴在命令行模式一栏中后,程序会从指定的第一话开始,到指定的最后一话结尾(例如从01到12),依次将shell命令中含有01的字符串替换为02、03等,直到12,并分别封装。
命令行模式由于缺乏文件对象支持,只用了简单的字符串替换,使用时请排除文件名中可能有的其他数字干扰。
四、一些其他
1、程序打开时默认载入上次成功封装的参数设置。
2、默认使用gbk编码,遇到无法识别字符串时会转换到utf8模式,支持度方面无须担心。
3、默认的cmd监视窗体没有关闭,可以在其中监视mkvmerge的情况。但我也只是大概看了两眼混流命令格式,并不知道如何调整log级别,故当出现一些奇妙症状时(比如字幕轨出现错误,某行开始时间超过结束时间被识别为非法),在ui中是没法判明原因的,只能收到失败通知。
4、python打包用的是pyinstaller,本来cx_freeze的加载速度要快得多,但打包出来一看大小300MB...py虚拟机不是很熟,这方面如果有知道原因的大佬请告诉我解决方法。
五、最后说一下有关序列识别
简单分析,生成序列的问题可以抽象为以下问题:在给定m个文件中选取n个,并将这n个文件构造为一个成序列的树。由上易知共有m到n的排列种构造方法,我们需要做的是选取其中合适的那一个。
另外基于问题特殊性,还有以下几个需求:
- 要可以普遍适用于所有类型的文件对象。
- 搜索要支持模糊搜索近似文件,但输出结果必须要确保准确无误,否则封装后还要手动检查太受苦了。
- 文件名中可能有各种各样的字符,需要支持utf8编码。
- 需要能识别包含阿拉伯数字的序列,数字可能是格式化的或未格式化的(可能是1、2..或01、02...)
- 文件夹中可能有不相关文件干扰。
- 文件夹中相关文件也可能存在干扰,比如工程文件夹内常见的,同一个文件可能存在两份以上副本。
- 文件名内可能存在干扰,这一部分存在比较大的不确定性,比如不妨设想一下新番<国家队>如果出了蓝光,而一个厨力满满的外国肥宅压制了BDRip,并取名叫0216lovelove压制组,那么这个名字在识别第2和第16话的时候可能就会给我们造成困难。
简单思考了一下,最后的实现设计了一个LCS的动态规划+贪婪优化的DFS的方法,方向主要优化时间复杂度。假设输入文件名长度为K,需要生成长度为n的序列,则LCS部分的时间复杂度应该为O(nk^2),DFS中由于通常干扰只可能存在于压制组、以及各话标题中,不太可能存在n个文件的文件名,全都包含全部包含1到n所有数并打乱顺序这种变态测资存在,复杂度应该为O(n)。
有关DFS,其实这个并不是DFS,只是我不知道该叫什么,逻辑又与DFS略有相同而已。如果以常规的DFS逻辑实现,那么构建树应该是,在每次进入叶节点后做判断是否满足要求(依次上溯根节点,判断其是否能够组成一个由大到小的序列),这种实现方式毫无疑问成本太高了。故实际上用了贪婪来构造树,并且将回溯判断改为如果搜索不到符合条件的下一节点(则说明当前根节点有误)、则返回上上个节点,修改当前根节点并重新搜索,也就是说实际上树形是一直在变的,该叫什么我也不知道,姑且就叫DFS吧。反正最后用python写出来,timeit算了一下12个文件大概是50毫秒左右,算了就这样吧。
最后放一下减半识别序列段代码,测资以上文图示范例数据为例:
from os import path,walk
from re import sub
def search_file_list(inputfile,start_epi_t,end_epi_t):
if not type(inputfile) is str or not type(start_epi_t) is int or not type(end_epi_t) is int:
raise TypeError('INPUT ARGUMENTS TYPE ERROR')
inputfile = path.abspath(inputfile)
epi_number = end_epi_t - start_epi_t + 1
filedir,filename = path.split(inputfile)
ext_name = path.splitext(inputfile)[1]
if ext_name == '' or filedir == '':
raise Warning('INPUT ERROR')
if not path.exists(inputfile):
raise Warning('INPUT FILE DO NOT EXIST')
def dp_lcs(str_a, str_b):
len1 = len(str_a)
len2 = len(str_b)
dp = [[0 for x in range(len2 + 1)] for x in range(len1 + 1)]
for i in range(1, len1 + 1):
for j in range(1, len2 + 1):
dp[i][j] = dp[i-1][j-1] + 1 if str_a[i-1] == str_b[j-1] else max([dp[i-1][j], dp[i][j-1]])
return(dp[len1][len2])
for i in walk(filedir):
file_list,count = i[2],0;break
for i in range(len(file_list)):
i_ext = path.splitext(file_list[i-count])[1]
if i_ext != ext_name:
file_list.pop(i-count);count+=1
file_list.remove(filename)
count_list = []
for i in file_list:
count_list.append(dp_lcs(filename,i))
count_list_tmp = sorted(count_list)
std_number = sum(count_list_tmp[-epi_number:])//epi_number
lowend,count = int(std_number*0.8+0.5),0
for i in range(len(file_list)):
if not lowend<=count_list[i]:
file_list.pop(i-count);count+=1
# preliminary screened
masked_file_list = []
for i in file_list:
masked_file_list.append(sub('([Yy]?[Uu]?[Vv]?4[42][40][Pp]?[\d]*)|([MmHh][AaIi][\d]+[Pp])|([\d]{3,4}[Pp])', '****', i))
output_list = [filename]
current_num = start_epi_t+1
current_lst = [[masked_file_list[:],list(range(len(masked_file_list)))]]
target_num = end_epi_t - start_epi_t + 1
while(True):
if len(output_list) == target_num:
break
current_count = current_num-start_epi_t-1
if current_count == -1:
print('NOT FIND')
return None
current_name = output_list[current_count]
current_search = [str(current_num).zfill(2),str(current_num)]
for ia,subfilenames in enumerate(current_lst[current_count][0]):
search_str = str(current_num)
if search_str.zfill(2) in subfilenames:
snum = subfilenames.index(search_str.zfill(2))
if subfilenames[snum:snum+3].isdigit():
current_lst[current_count][0][ia] = current_lst[current_count][0][ia][:snum]+'*'+current_lst[current_count][0][ia][snum+1:];break
tmp_lst = [[],[]]
for j,t in enumerate(current_lst[current_count][0]):
num = current_lst[current_count][1][j]
if t != subfilenames:
tmp_lst[0].append(masked_file_list[num])
tmp_lst[1].append(num)
else:
output_list.append(file_list[num])
current_lst.append(tmp_lst)
current_num+=1
break
else:
current_lst.pop(-1)
current_num -= 1
target = output_list.pop(-1)
for ib,tb in enumerate(file_list):
if target == tb:
ind = current_lst[-1][1].index(ib)
cname = current_lst[-1][0][ind]
cnum = str(current_num).zfill(2)
if cnum in cname:
nnum = cname.index(cnum.zfill(2))
current_lst[-1][0][ind] = current_lst[-1][0][ind][:nnum]+'*'+current_lst[-1][0][ind][nnum+1:]
# process finished
if len(output_list) == end_epi_t-start_epi_t+1:
return output_list
else:
print('NOT FIND')
return None
if __name__ == '__main__':
inputfile = r'C:\test\[Encoder05] 冴えない彼女の育てかた♭ #01 冴えない竜虎の相見えかた (BDRip.HEVC.1080P.YUV420P10.QAAC).mkv'
output_list = search_file_list(inputfile,1,11)
for i in output_list:
print(i)
[...]封装工具:https://www.nazorip.site/archives/76/[...]
[...]封装工具:https://www.nazorip.site/archives/76/[...]
[...]封装工具:https://www.nazorip.site/archives/76/[...]
[...]封装工具:https://www.nazorip.site/archives/76/[...]
[...]封装工具:https://www.nazorip.site/archives/76/[...]
[...]封装工具:https://www.nazorip.site/archives/76/[...]
[...]封装工具:https://www.nazorip.site/archives/76/[...]
[...]封装工具:https://www.nazorip.site/archives/76/[...]
[...]封装工具:https://www.nazorip.site/archives/76/[...]
[...]封装工具:https://www.nazorip.site/archives/76/[...]
封装数量是不是想限定在99以内,超过的话有方法吗
[...]封装工具:https://www.nazorip.site/archives/76/[...]
[...]封装工具:https://www.nazorip.site/archives/76/[...]