NAZOrip 批量封装工具 说明 & 下载

F@NAZOrip
F@NAZOrip 2018年07月16日
  • 在其它设备中阅读本文章
  • 下载地址位于文末 *

一、前言 & 功能简介

    由于我组封装用量不大(通常不会在同一视频下封装多条音轨、字幕轨道),但对封装要求较高(需要指明轨道语言、轨道作者等),而通常封装面对十几个或几十个文件。针对我组情况,开发了这套支持对批量序列文件封装、支持文件名格式化输出的工具。

    封装工具功能单纯,做了图形界面,过程一如既往的繁琐。


二、使用说明

    如上图所示,本工具基于mkvtoolnix,系mkvmerge的图形界面(GUI)。如有需求,使用前请先到官方地址下载mkvtoolnix,并在GUI中选择安装目录。文件夹内虽然已经附带mkvmerge,确保用户未安装mkvtools时也能正常使用功能,但由于本界面以后大概率不会更新,不保证内核最新。

    简单使用步骤:

  1. (可选)安装MKVToolNix。
  2. 下载、解压、运行软件。
  3. 将待封装的成序列文件,分别置于同一文件夹。
    (* 例如:要封装12话动画,需将12个视频流放置于同一文件夹,音频流同理。视频与音频不必放在同一文件夹)
  4. 在GUI中选择起止话数,例如5到12话。
  5. 将你选择的第一话(如上一例中的第五话)的视频、音频、字幕、章节文件输入指定位置,并指定各轨道名称。
  6. 点击开始封装,并等待封装完成。

三、部分实现逻辑(进阶向)

    总的来说,界面能实现的逻辑分为两种模式,分别是图形界面(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的排列种构造方法,我们需要做的是选取其中合适的那一个。

另外基于问题特殊性,还有以下几个需求:

  1. 要可以普遍适用于所有类型的文件对象。
  2. 搜索要支持模糊搜索近似文件,但输出结果必须要确保准确无误,否则封装后还要手动检查太受苦了。
  3. 文件名中可能有各种各样的字符,需要支持utf8编码。
  4. 需要能识别包含阿拉伯数字的序列,数字可能是格式化的或未格式化的(可能是1、2..或01、02...)
  5. 文件夹中可能有不相关文件干扰。
  6. 文件夹中相关文件也可能存在干扰,比如工程文件夹内常见的,同一个文件可能存在两份以上副本。
  7. 文件名内可能存在干扰,这一部分存在比较大的不确定性,比如不妨设想一下新番<国家队>如果出了蓝光,而一个厨力满满的外国肥宅压制了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)

六、下载地址

Github / BAIDU 密码:rbmv/NAZOrip
mkvmerge不保证最新。