├── README.md ├── docker-compose.yml └── scripts ├── .ipynb_checkpoints └── encode-checkpoint.ipynb ├── encode.ipynb ├── hlw └── jupyter.sh /README.md: -------------------------------------------------------------------------------- 1 | # Docker-VapourSynth-Yuuno 2 | 3 | ## FRDS压制简明教程 4 | 5 | 1. 自行准备Linux环境与docker环境,包括docker-compose 6 | 2. `git clone https://github.com/gzycode39/docker-vapoursynth-yuuno.git` 7 | 3. `cd docker-vapoursynth-yuuno` 8 | 4. 将docker-compose.yml中的`/YOUR/ENCODE/PATH`修改为你的工作目录(即原盘所在目录),假设原盘名为`HLWYYDS.2021.HLW`,宿主机目录结构为`/home/hlw/HLW/HLWYYDS.2021.HLW/`,则工作目录应为`/home/hlw/HLW` 9 | 5. `docker-compose up -d` 10 | 6. `docker exec -it vapoursynth-yuuno /bin/bash` 11 | 7. `frds /encode/HLWYYDS.2021.HLW/` 12 | 8. 按照提示选择轨道 13 | 9. 耐心等待提取完成,在浏览器输入`你的机器ip:8888`(初始密码123),打开名为`HLWYYDS.2021.HLW`的文件,修改第9行`border= xxx`切边条数,有需要可自行添加脏边修复代码,在浏览器中预览确认无误后,回到命令行界面按任意键继续 14 | 10. 进入宿主机目录`/home/hlw/HLW/out-HLWYYDS.2021.HLW`查看成片,种子及mediainfo位于`torrent`目录下 15 | 11. 发布成片 16 | 17 | ## Software Versions 18 | 19 | - python3: 3.8.5 20 | - eac3to: v3.34 21 | - x265: v3.5 22 | - mkvmerge: v53.0.0 23 | - mediainfo: v19.09 24 | - mktorrent: v1.1 25 | 26 | ## How to build the environment 27 | 28 | ``` 29 | docker-compose up -d 30 | ``` 31 | 32 | ## How to enter the container 33 | 34 | ``` 35 | docker exec -it vapoursynth-yuuno /bin/bash 36 | ``` 37 | 38 | ## How to encode a movie 39 | 40 | Please refer to [WhaleHu/Encode-guide-frds](https://github.com/WhaleHu/Encode-guide-frds) 41 | 42 | ## How to use eac3to to demux video tracks, audio tracks, and so on 43 | 44 | ``` 45 | eac3to-demux /PATH/TO/BDMV 46 | ``` 47 | 48 | ## How to encode TV series 49 | 50 | ``` 51 | encode_series "/PATH/TO/SERIES/REMUX" 52 | ``` 53 | 54 | ## How to convert dts to ac3 55 | 56 | ``` 57 | eac3to "xxx.dtsma" "xxx.ac3" 58 | ``` 59 | 60 | ## How to use yuuno 61 | 62 | 1. Access your_ip:8888, the default password is __123__. 63 | 64 | 2. Open the notebook __encode.ipynb__ 65 | 66 | 3. to be continued 67 | 68 | # Tutorial from Krita 69 | 70 | ### 一.环境准备 71 | 72 | #### 1.Docker部署 73 | 74 | ##### ①.拉取文件: 75 | 76 | ``` 77 | git clone https://github.com/gzycode39/docker-vapoursynth-yuuno.git 78 | ``` 79 | 80 | ##### ②.进入项目目录 81 | 82 | ``` 83 | cd docker-vapoursynth-yuuno 84 | ``` 85 | 86 | ##### ③.启动容器: 87 | 88 | ``` 89 | docker-compose up -d 90 | ``` 91 | 92 | ##### ④.进入容器: 93 | 94 | ``` 95 | docker exec -it vapoursynth-yuuno /bin/bash 96 | ``` 97 | 98 | ##### ⑤.删除容器 99 | 100 | ``` 101 | docker-compose ps 102 | 103 | docker-compose down 104 | ``` 105 | 106 | ##### ⑥.更新镜像 107 | 108 | ``` 109 | docker pull yyfyyf/vapoursynth-yuuno:v0.X 110 | X为最新版本号 111 | ``` 112 | 113 | #### 2.拉取原盘,压片前的装备 114 | 115 | ##### ①.拉取原盘文件至容器外对应/encode的目录下 116 | 117 | ##### ②.查看原盘目录结构 118 | 119 | ``` 120 | eac3to 目录 121 | ``` 122 | 123 | ##### ③.找到对应的(最大的)mpls,并提取其全部文件到当前目录 124 | 125 | ``` 126 | eac3to-demux 目录 127 | ``` 128 | 129 | ##### ④.进入jupyter notebook 130 | 131 | ``` 132 | ip:映射端口 133 | ``` 134 | 135 | ### 二.压制样片 136 | 137 | #### 1.使用现成脚本或新建python3脚本 138 | 139 | ``` 140 | 先输一行%load_ext yuuno 141 | 然后下面输入%%vspreview再接脚本代码 142 | run 143 | ``` 144 | 145 | #### 2.查看并记录原片及样片帧数,进行切黑边,修脏边等操作 146 | 147 | 148 | 149 | #### 3.压制样片 150 | 151 | 把ipynb的脚本内容复制一份到vpy,修改sh脚本中的帧数为样片帧数,修改vpy为对应vpy 152 | 153 | 运行sh脚本,压制样片 154 | 155 | 观察码率 156 | 157 | 158 | 159 | ### 三.样片与原片进行对比 160 | 161 | #### 1.在ipynb加入抽取对比 162 | 163 | 164 | 165 | #### 2.对比样片图片及原片图片 166 | 167 | 168 | 169 | #### 3.根据情况对crf值进行0.5步进微调 170 | 171 | 172 | 173 | #### 4.可以则开始正片压制,不可则重新压制样片,重新进行对比 174 | 175 | 176 | 177 | ### 四.压制成片 178 | 179 | #### 1.修改sh脚本中帧数为原片帧数,执行sh脚本进行压制 180 | 181 | #### 2.等待压制完成 182 | 183 | 184 | 185 | #### 五.进行成片与原片对比 186 | 187 | 188 | 189 | ### 六.混流封装及制种 190 | 191 | #### 1.音轨转换 192 | 193 | ``` 194 | eac3to "原音轨.dtsma" 新音轨.ac3 195 | ``` 196 | 197 | #### 2.混流封装 198 | 199 | ``` 200 | mkvmerge -o "电影名.年份.bluray.1080p.x265.10bit.MNHD-FRDS.mkv" --title "电影名 [年份] Bluray 1080p MNHD-FRDS" --chapters "原盘目录.txt" --compression 0:none --default-duration 0:24000/1001fps --track-name 0:作者署名 成片.hevc --compression 0:none --default-track 0 --language 0:ISO编码 音轨.ac3 --compression 0:none --default-track 0 --language 0:chi --track-name 0:CHS "简中字幕.sup" --compression 0:none --language 0:chi --track-name 0:CHT "繁中字母.sup" --compression 0:none --language 0:原始字幕语言编码 --track-name 0:原始字幕语言编码 "原始语言字幕.sup" 201 | ``` 202 | 203 | #### 3.制种 204 | 205 | ``` 206 | mktorrent -a tracker地址 -l 22 -p 成片目录 207 | ``` 208 | 209 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | services: 3 | vapoursynth-yuuno: 4 | image: yyfyyf/vapoursynth-yuuno:v1.4 5 | container_name: vapoursynth-yuuno 6 | environment: 7 | - PUID=1000 8 | - PGID=1000 9 | volumes: 10 | - /YOUR/ENCODE/PATH:/encode 11 | - ./scripts:/scripts 12 | - ./scripts/hlw:/usr/local/bin/hlw 13 | ports: 14 | - 8888:8888 15 | privileged: true 16 | restart: unless-stopped 17 | tty: true 18 | entrypoint: ["./jupyter.sh"] 19 | working_dir: /scripts -------------------------------------------------------------------------------- /scripts/.ipynb_checkpoints/encode-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%load_ext yuuno" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "%%vspreview\n", 19 | "import vapoursynth as vs\n", 20 | "import havsfunc as haf\n", 21 | "import mvsfunc as mvf\n", 22 | "#导入相关函数\n", 23 | "\n", 24 | "core = vs.get_core()\n", 25 | "\n", 26 | "#请依据自己内存调整(单位MB)\n", 27 | "core.max_cache_size = 27384\n", 28 | "\n", 29 | "#加载片源\n", 30 | "src = core.ffms2.Source(source=r'/encode/After.the.Storm.2016.BluRay.Remux.1080p.AVC.TrueHD.5.1-HiFi.mkv')\n", 31 | "\n", 32 | "#另一种加载片源,输入即为16bit色深\n", 33 | "#src = core.lsmas.LWLibavSource(source=r\"/encode/After.the.Storm.2016.BluRay.Remux.1080p.AVC.TrueHD.5.1-HiFi.mkv\",format=\"yuv420p16\")\n", 34 | "\n", 35 | "#处理前调整为16bit色深,可以降低运算带来的损失\n", 36 | "src = mvf.Depth(src, 16)\n", 37 | "\n", 38 | "#切边\n", 39 | "src=core.std.Crop(src, left=0, right=0, top=20, bottom=20)\n", 40 | "\n", 41 | "#脏边处理相关\n", 42 | "#src = core.fb.FillBorders(src, 0, 1, 0, 0, mode=\"fillmargins\")\n", 43 | "#src = core.edgefixer.Continuity(src,left=4, right=4)\n", 44 | "\n", 45 | "#将分辨率(压制为720p)\n", 46 | "#src=core.resize.Spline36(src, 1280, 692)\n", 47 | "\n", 48 | "#转为10bit再输出,x265一般压制为10bit,x264为8bit\n", 49 | "src = mvf.Depth(src, 10)\n" 50 | ] 51 | } 52 | ], 53 | "metadata": { 54 | "kernelspec": { 55 | "display_name": "Python 3", 56 | "language": "python", 57 | "name": "python3" 58 | }, 59 | "language_info": { 60 | "codemirror_mode": { 61 | "name": "ipython", 62 | "version": 3 63 | }, 64 | "file_extension": ".py", 65 | "mimetype": "text/x-python", 66 | "name": "python", 67 | "nbconvert_exporter": "python", 68 | "pygments_lexer": "ipython3", 69 | "version": "3.8.5" 70 | } 71 | }, 72 | "nbformat": 4, 73 | "nbformat_minor": 4 74 | } 75 | -------------------------------------------------------------------------------- /scripts/encode.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%load_ext yuuno" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "%%vspreview\n", 19 | "import vapoursynth as vs\n", 20 | "import havsfunc as haf\n", 21 | "import mvsfunc as mvf\n", 22 | "#导入相关函数\n", 23 | "\n", 24 | "core = vs.get_core()\n", 25 | "\n", 26 | "#请依据自己内存调整(单位MB)\n", 27 | "core.max_cache_size = 27384\n", 28 | "\n", 29 | "#加载片源\n", 30 | "src = core.ffms2.Source(source=r'/encode/After.the.Storm.2016.BluRay.Remux.1080p.AVC.TrueHD.5.1-HiFi.mkv')\n", 31 | "\n", 32 | "#另一种加载片源,输入即为16bit色深\n", 33 | "#src = core.lsmas.LWLibavSource(source=r\"/encode/After.the.Storm.2016.BluRay.Remux.1080p.AVC.TrueHD.5.1-HiFi.mkv\",format=\"yuv420p16\")\n", 34 | "\n", 35 | "#处理前调整为16bit色深,可以降低运算带来的损失\n", 36 | "src = mvf.Depth(src, 16)\n", 37 | "\n", 38 | "#切边\n", 39 | "src=core.std.Crop(src, left=0, right=0, top=20, bottom=20)\n", 40 | "\n", 41 | "#脏边处理相关\n", 42 | "#src = core.fb.FillBorders(src, 0, 1, 0, 0, mode=\"fillmargins\")\n", 43 | "#src = core.edgefixer.Continuity(src,left=4, right=4)\n", 44 | "\n", 45 | "#将分辨率(压制为720p)\n", 46 | "#src=core.resize.Spline36(src, 1280, 692)\n", 47 | "\n", 48 | "#转为10bit再输出,x265一般压制为10bit,x264为8bit\n", 49 | "src = mvf.Depth(src, 10)\n" 50 | ] 51 | } 52 | ], 53 | "metadata": { 54 | "kernelspec": { 55 | "display_name": "Python 3", 56 | "language": "python", 57 | "name": "python3" 58 | }, 59 | "language_info": { 60 | "codemirror_mode": { 61 | "name": "ipython", 62 | "version": 3 63 | }, 64 | "file_extension": ".py", 65 | "mimetype": "text/x-python", 66 | "name": "python", 67 | "nbconvert_exporter": "python", 68 | "pygments_lexer": "ipython3", 69 | "version": "3.8.5" 70 | } 71 | }, 72 | "nbformat": 4, 73 | "nbformat_minor": 4 74 | } 75 | -------------------------------------------------------------------------------- /scripts/hlw: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | import sys 4 | import re 5 | import json 6 | import time 7 | import numpy as np 8 | 9 | 10 | def usage(): 11 | print("\033[1;31mUSAGE: " + sys.argv[0].split("/")[-1] + " /PATH\033[0m") 12 | 13 | 14 | eac3to = "eac3to" 15 | ffmpeg = "ffmpeg" 16 | vstest_path = "/usr/local/etc/vstest" 17 | external_sub = [ 18 | # [name, language, filanem, suffix, default], 19 | # ["chs&eng(srt)", "mul", "chs_eng", "srt", True], 20 | # ["cht(ass)", "chi", "cht", "ass", False], 21 | ] 22 | 23 | ipy = """ 24 | { 25 | "cells": [ 26 | { 27 | "cell_type": "code", 28 | "execution_count": 1, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "%load_ext yuuno" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 2, 38 | "metadata": {}, 39 | "outputs": [ 40 | { 41 | "data": { 42 | "application/vnd.jupyter.widget-view+json": { 43 | "model_id": "3c6b7d52588b4efb9f79bbcfae690802", 44 | "version_major": 2, 45 | "version_minor": 0 46 | }, 47 | "text/plain": [ 48 | "Preview(clip=, frame=7422)" 49 | ] 50 | }, 51 | "metadata": {}, 52 | "output_type": "display_data" 53 | } 54 | ], 55 | "source": [ 56 | "%%vspreview\\n", 57 | "import vapoursynth as vs\\n", 58 | "import havsfunc as haf\\n", 59 | "import mvsfunc as mvf\\n", 60 | "core = vs.core\\n", 61 | "core.max_cache_size = 10240\\n", 62 | "encode_x264 = False\\n", 63 | "src = core.lsmas.LWLibavSource(source=r'REPLACE_PATH')\\n", 64 | "src = mvf.Depth(src, 16)\\n", 65 | "border = 130\\n", 66 | "def zresize(clip, preset=None, width=None, height=None, left=0, right=0, top=0, bottom=0, ar=16 / 9,\\n", 67 | " **kwargs):\\n", 68 | " if preset:\\n", 69 | " if clip.width / clip.height > ar:\\n", 70 | " return zresize(clip, width=ar * preset, left=left, right=right, top=top, bottom=bottom, **kwargs)\\n", 71 | " else:\\n", 72 | " return zresize(clip, height=preset, left=left, right=right, top=top, bottom=bottom, **kwargs)\\n", 73 | "\\n", 74 | " if (width is None) and (height is None):\\n", 75 | " width = clip.width\\n", 76 | " height = clip.height\\n", 77 | " rh = rw = 1\\n", 78 | " elif width is None:\\n", 79 | " rh = rw = height / (clip.height - top - bottom) \\n", 80 | " elif height is None:\\n", 81 | " rh = rw = width / (clip.width - left - right)\\n", 82 | " else:\\n", 83 | " rh = height / clip.height\\n", 84 | " rw = width / clip.width\\n", 85 | "\\n", 86 | " w = round(((clip.width - left - right) * rw) / 2) * 2\\n", 87 | " h = round(((clip.height - top - bottom) * rh) / 2) * 2\\n", 88 | " resizer = core.resize.Spline36\\n", 89 | " return resizer(clip=clip, width=w, height=h, src_left=left, src_top=top, src_width=clip.width - left - right,\\n", 90 | " src_height=clip.height - top - bottom, dither_type=\\"error_diffusion\\", **kwargs)\\n", 91 | "\\n", 92 | "src=core.std.Crop(src, left=0, right=0, top=border, bottom=border)\\n", 93 | "#src = core.fb.FillBorders(src, 0, 1, 0, 0, mode=\\"fillmargins\\")\\n", 94 | "#src = core.edgefixer.Continuity(src,left=4, right=4)\\n", 95 | "if encode_x264:\\n", 96 | " src=zresize(src, preset=REPLACE_PRESET)\\n", 97 | "src = mvf.Depth(src, 10)\\n", 98 | "src.set_output()\\n" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [] 107 | } 108 | ], 109 | "metadata": { 110 | "kernelspec": { 111 | "display_name": "Python 3", 112 | "language": "python", 113 | "name": "python3" 114 | }, 115 | "language_info": { 116 | "codemirror_mode": { 117 | "name": "ipython", 118 | "version": 3 119 | }, 120 | "file_extension": ".py", 121 | "mimetype": "text/x-python", 122 | "name": "python", 123 | "nbconvert_exporter": "python", 124 | "pygments_lexer": "ipython3", 125 | "version": "3.8.5" 126 | } 127 | }, 128 | "nbformat": 4, 129 | "nbformat_minor": 4 130 | } 131 | """ 132 | 133 | if (len(sys.argv) != 2): 134 | usage() 135 | exit(-1) 136 | 137 | x265_parameter = " -D 10 --preset veryslow --high-tier --ctu 32 --rd 4 --subme 7 --ref 4 --merange=57 --me 3 --qg-size 8 --weightb --pmode --no-rect --no-amp --rskip 0 --tu-intra-depth 4 --tu-inter-depth 4 --range limited --no-open-gop --no-sao --no-early-skip --min-keyint=1 --rc-lookahead 80 --no-cutree --bframes 6 --vbv-bufsize 160000 --vbv-maxrate 160000 --colorprim bt709 --transfer bt709 --colormatrix bt709 --deblock -3:-3 --ipratio 1.3 --pbratio 1.2 --qcomp 0.65 --aq-mode 1 --aq-strength 1 --psy-rd 1.50 --psy-rdoq 1.00 --cbqpoffs -2 --crqpoffs -2" 138 | x264_parameter = " --preset placebo --level 41 --vbv-bufsize 78125 --vbv-maxrate 62500 --merange 32 --bframes 16 --deblock -3:-3 --no-fast-pskip --rc-lookahead 250 --qcomp 0.60 --psy-rd 1.02:0.00 --aq-mode 3 --aq-strength 0.80 --me umh --b-adapt 2 --direct auto --subme 11 --trellis 2 --no-dct-decimate --no-mbtree --colormatrix bt709 --colorprim bt709 --ipratio 1.30 --pbratio 1.20" 139 | 140 | path = sys.argv[1] 141 | if (not path.startswith("/")): 142 | path = os.getcwd() + "/" + path 143 | 144 | cwd = os.getcwd() 145 | title = path.strip("/").split("/")[-1] 146 | output_dir = "/encode/out-" + title 147 | os.system("mkdir -p %s" % output_dir) 148 | init_done = False 149 | chapter_track = 1 150 | video_track = 2 151 | audio_track = [] 152 | content = [] 153 | sub_track = [] 154 | eac_cmd = "" 155 | mkv_extract = "" 156 | is_mkv = False 157 | encoder = 'HLW' 158 | 159 | conf = {"x265": 7500, "480": 2000, "576": 3500, "720": 6500, "1080": 11000} 160 | 161 | 162 | def format_time(t): 163 | hour = int(t / 3600) 164 | t1 = t % 3600 165 | minute = int(t1 / 60) 166 | return "{}小时{}分".format(hour, minute) 167 | 168 | 169 | def check_tracks(): 170 | print("您选择的轨道如下:") 171 | print("\033[1;31mVideo track: %d\n\033[32mAudio track:\033[0m" % (video_track)) 172 | for it in audio_track: 173 | print("\033[32m%d (%s) (%s => %s)\033[0m" % (it[0], it[1], it[2], it[3])) 174 | print("\033[33mSubtitle track:\033[0m") 175 | for it in sub_track: 176 | is_default = " default" if it[3] else "" 177 | print("\033[1;33m%d (%s) (%s)%s\033[0m" % (it[0], it[1], it[2], is_default)) 178 | if external_sub: 179 | print("\033[33mExternal subtitle track:\033[0m") 180 | for it in external_sub: 181 | is_default = " default" if it[4] else "" 182 | print("\033[1;33m%s (%s) (%s) (%s)%s\033[0m" % (it[0], it[1], it[2], it[3], is_default)) 183 | choice = input("\033[1;36m确认上述信息无误(Y/n) (Y: 进入下一步 n: 重新进行选择): \033[0m") 184 | if choice == 'Y' or choice == 'y': 185 | return True 186 | return False 187 | 188 | 189 | def check_resolution(): 190 | print("您要压制的选项如下:") 191 | for k, v in conf.items(): 192 | if v != 0: 193 | mode = "x264" 194 | resolution = "1080" 195 | if k == "x265": 196 | mode = "x265" 197 | else: 198 | resolution = k 199 | print("\033[1;34m{} ({}p): 帧率 {}\033[0m".format(mode, resolution, v)) 200 | choice = input("\033[1;36m确认上述信息无误(Y/n) (Y: 进入下一步 n: 重新进行选择): \033[0m") 201 | if choice == 'Y' or choice == 'y': 202 | return True 203 | return False 204 | 205 | 206 | def match_info(num, res): 207 | for line in res: 208 | if re.match('^%d:(.*)?' % num, line): 209 | return line 210 | 211 | 212 | def match_lang(num, res): 213 | return match_info(num, res).split(',')[1].strip() 214 | 215 | 216 | def match_item_name(num, res): 217 | for line in res: 218 | if re.match('^%d:(.*)?' % num, line): 219 | return line.split(':')[1].split(',')[0].strip() 220 | 221 | 222 | def check_subtitle(num, res): 223 | return match_item_name(num, res).startswith("Subtitle") 224 | 225 | 226 | def select_resolution(): 227 | global conf 228 | print("请选择您要压制的作品目标帧率,输入\033[1;31m0\033[0m表示不压制该选项") 229 | choice = input("请输入x265 (1080p)的目标帧率(默认为\033[1;32m%s\033[0m):" % conf["x265"]) 230 | if choice and choice.isdigit(): 231 | conf["x265"] = int(choice) 232 | 233 | for resolution in ["480", "576", "720", "1080"]: 234 | choice = input("请输入x264 (%sp)的目标帧率(默认为\033[1;32m%s\033[0m):" % (resolution, conf[resolution])) 235 | if choice and choice.isdigit(): 236 | conf[resolution] = int(choice) 237 | 238 | with open("{}/config.json".format(output_dir), "w") as f: 239 | f.write(json.dumps(conf, indent=4)) 240 | 241 | 242 | def init(path): 243 | global init_done, chapter_track, video_track, audio_track, sub_track, content, conf, eac_cmd, mkv_extract, is_mkv 244 | is_mkv = True if path.endswith('mkv') else False # 判断源是否为mkv文件 245 | chapter_track = 1 if not is_mkv else 998 246 | video_track = 2 247 | audio_track = [] 248 | sub_track = [] 249 | 250 | if not content: 251 | eac_cmd = eac3to + ' "' + title + '"' 252 | mkv_extract = 'mkvextract "{}" tracks '.format(title) 253 | res = os.popen(eac_cmd).read() 254 | content = [] 255 | for line in res.split("\n")[:-1]: 256 | if line.startswith("analyze"): 257 | continue 258 | content.append(line) 259 | print('\n'.join(content)) 260 | if not is_mkv: 261 | mpls_num = 0 262 | for line in content: 263 | if re.match(r'^(\d)+\).*', line): 264 | mpls_num += 1 265 | choice = input("请选择mpls (1-%d): " % mpls_num) 266 | eac_cmd = eac3to + ' "' + title + '" "%s)"' % choice 267 | res = os.popen(eac_cmd).read() 268 | content = [] 269 | for line in res.split("\n")[:-1]: 270 | if line.startswith("analyze"): 271 | continue 272 | content.append(line) 273 | for line in content: 274 | if re.match(r'(.*)?Chapters(.*)?', line): 275 | chapter_track = int(line.split(":")[0]) 276 | if re.match(r'(.*)?h264(.*)?', line): 277 | video_track = int(line.split(":")[0]) 278 | print("%s" % line) 279 | done = False 280 | while not done: 281 | tmp_audio_track = [2, "eng", "eac3to", "ac3", "", ""] # [音轨号,音轨语言,提取方案,目标格式,音频质量调整,音频名称 ] 282 | choice = input("请输入你想添加的音轨编号 (输入 q 以停止添加音频): ") 283 | if choice == 'q': 284 | done = True 285 | break 286 | if choice.isdigit(): 287 | tmp_audio_track[0] = int(choice) 288 | else: 289 | continue 290 | tmp_audio_track[1] = match_lang(tmp_audio_track[0], content)[:3].lower() 291 | tmp_info = match_info(tmp_audio_track[0], content) 292 | if "DTS" in tmp_info or "PCM" in tmp_info or 'FLAC' in tmp_info: 293 | tmp_audio_track[2] = "eac3to" 294 | if "5.1 channels" in tmp_info: 295 | tmp_audio_track[3] = "ac3" 296 | elif "2.0 channels" in tmp_info: 297 | tmp_audio_track[3] = "flac" 298 | tmp_audio_track[4] = " -down16" 299 | if "EAC3" in tmp_info: 300 | tmp_audio_track[2] = "ffmpeg" 301 | tmp_audio_track[3] = "eac3" 302 | choice = input("请输入音轨语言(默认为\033[1;32m%s\033[0m): " % tmp_audio_track[1]) 303 | if choice: 304 | tmp_audio_track[1] = choice 305 | audio_track.append(tmp_audio_track) 306 | choice = input('请输入音轨名称(默认为无):') 307 | if choice: 308 | tmp_audio_track[5] = choice 309 | commentary_track = False 310 | choice = input("是否进行评论音轨转码?(Y/n) (默认为n): ") 311 | if choice == 'y'.upper(): 312 | commentary_track = True 313 | if commentary_track: 314 | tmp_audio_track[4] = " -192" 315 | tmp_audio_track[3] = 'ac3' 316 | done = False 317 | choice = "" 318 | while not done and not choice: 319 | choice = input("请输入你想添加的字幕编号(输入 q 以停止添加字幕): ") 320 | if choice == 'q': 321 | done = True 322 | break 323 | if choice.isdigit(): 324 | num = int(choice) 325 | if check_subtitle(num, content): 326 | name = match_lang(num, content) 327 | lang = name[:3].lower() 328 | else: 329 | continue 330 | else: 331 | continue 332 | 333 | choice = input("请输入字幕语言(默认为\033[1;32m%s\033[0m): " % lang) 334 | if choice: 335 | if choice == 'q': 336 | done = True 337 | break 338 | lang = choice 339 | 340 | choice = input("请输入字幕名(默认为\033[1;32m%s\033[0m): " % name) 341 | if choice: 342 | if choice == 'q': 343 | done = True 344 | break 345 | name = choice 346 | 347 | is_default = False 348 | choice = input("设置为默认字幕轨道吗?(Y/n) (默认为n): ") 349 | if choice: 350 | if choice == 'Y' or choice == 'y': 351 | is_default = True 352 | num = num - 1 if is_mkv else num # mkvextract 使用的编号比eac3to少一位 353 | sub_track.append([num, lang, name, is_default, is_mkv]) 354 | choice = "" 355 | 356 | init_done = check_tracks() 357 | 358 | if "config.json" not in os.listdir(output_dir): 359 | select_resolution() 360 | else: 361 | with open("{}/config.json".format(output_dir)) as f: 362 | conf = json.loads(f.read()) 363 | 364 | if not check_resolution(): 365 | select_resolution() 366 | 367 | 368 | def demux(path): 369 | global mkv_extract 370 | cmd = eac_cmd 371 | 372 | cmd += ' %d: "%s_%d.264"' % (video_track, title, video_track) 373 | ffmpeg_cmd = "" 374 | for audio in audio_track: 375 | if audio[2] == "eac3to": 376 | cmd += ' %d: "%s_%d%s.%s"%s' % (audio[0], title, audio[0], audio[1], audio[3], audio[4]) 377 | elif audio[2] == "ffmpeg": 378 | if audio[3] == "eac3": 379 | ffmpeg_cmd = '"%s" -i "%s" -map 0:%d -c:a eac3 -b:a 640k -dsurex_mode 2 "%s_%d%s.%s"' % ( 380 | ffmpeg, mkv, audio[0] - 1, title, audio[0], audio[1], audio[3]) 381 | else: 382 | print("Audio format error %d: %s" % (audio[0], audio[3])) 383 | exit(-1) 384 | print(cmd) 385 | for sub in sub_track: 386 | if not sub[4]: 387 | cmd += ' %d: "%s_%d%s.sup"' % (sub[0], title, sub[0], sub[1]) 388 | else: 389 | mkv_extract += ' %d:"%s_%d%s.sup"' % (sub[0], title, sub[0], sub[1]) 390 | if is_mkv: 391 | mkv_extract += ' chapters "%s_%d.txt"' % (title, chapter_track) 392 | else: 393 | cmd += ' %d: "%s_%d.txt"' % (chapter_track, title, chapter_track) 394 | os.system(cmd) 395 | if ffmpeg_cmd: 396 | os.system(ffmpeg_cmd) 397 | if is_mkv: 398 | os.system(mkv_extract) 399 | 400 | 401 | def move(src, dst): 402 | cmd = 'mv "%s" "%s"' % (src, dst) 403 | os.system(cmd) 404 | 405 | 406 | def move_files(src, dst): 407 | move("%s/%s_%d - Log.txt" % (src, title, chapter_track), dst) 408 | move("%s/%s_%d.txt" % (src, title, chapter_track), dst) 409 | move("%s/%s_%d.264" % (src, title, video_track), dst) 410 | for audio in audio_track: 411 | move("%s/%s_%d%s.%s" % (src, title, audio[0], audio[1], audio[3]), dst) 412 | for sub in sub_track: 413 | move("%s/%s_%d%s.sup" % (src, title, sub[0], sub[1]), dst) 414 | 415 | 416 | class Encoder(object): 417 | def __init__(self, encode_script, merge_script): 418 | self.encode_script = encode_script 419 | self.merge_script = merge_script 420 | 421 | def encode(self): 422 | print("bash {}".format(self.encode_script)) 423 | os.system("bash {}".format(self.encode_script)) 424 | 425 | def merge(self): 426 | print("bash {}".format(self.merge_script)) 427 | os.system("bash {}".format(self.merge_script)) 428 | 429 | 430 | def generate_merge_script(name, group=encoder): 431 | merge_script = "{}/{}-{}-merge.sh".format(output_dir, title, name) 432 | x264 = name != "x265" 433 | with open(merge_script, "w") as f: 434 | year = re.search("\.\d{4}\.", title) 435 | if x264: 436 | cmd = 'mkvmerge -o "%s/%s.mkv" \\\n --title "%s" \\\n' % ( 437 | output_dir, title[0:year.span()[1]] + "{}p.BluRay.DD5.1.x264-{}".format(name, group), 438 | title[0:year.span()[0]].replace(".", " ") + " [" + year.group(0).strip(".") + "] {}p BluRay-{}".format( 439 | name, 440 | group)) 441 | else: 442 | cmd = 'mkvmerge -o "%s/%s.mkv" \\\n --title "%s" \\\n' % ( 443 | output_dir, title[0:year.span()[1]] + "1080p.BluRay.x265.10bit.{}-FRDS".format(group), 444 | title[0:year.span()[0]].replace(".", " ") + " [" + year.group(0).strip( 445 | ".") + "] BluRay 1080p {}-FRDS".format(group)) 446 | if not x264: 447 | cmd += ' --ui-language zh_CN \\\n' 448 | cmd += ' --chapters "%s_%d.txt" \\\n' % (title, chapter_track) 449 | if x264: 450 | cmd += ' --language 0:und "%s-%s.avc" \\\n' % (title, name) 451 | else: 452 | cmd += ' --language 0:und "%s-%s.hevc" \\\n' % (title, name) 453 | default_audio = True 454 | for audio in audio_track: 455 | if default_audio: 456 | cmd += ' --default-track 0:yes' 457 | default_audio = False 458 | cmd += ' --track-name 0:"%s" --language 0:%s "%s_%d%s.%s" \\\n' % ( 459 | audio[5], audio[1], title, audio[0], audio[1], audio[3]) 460 | for sub in external_sub: 461 | if sub[4] and not x264: 462 | cmd += ' --default-track 0:yes' 463 | else: 464 | cmd += ' --default-track 0:no' 465 | cmd += ' --track-name 0:"%s" --language 0:%s "%s_%s.%s" \\\n' % ( 466 | sub[0], sub[1], title, sub[2], sub[3]) 467 | for sub in sub_track: 468 | if sub[3] and not x264: 469 | cmd += ' --default-track 0:yes' 470 | else: 471 | cmd += ' --default-track 0:no' 472 | cmd += ' --track-name 0:"%s" --language 0:%s "%s_%d%s.sup" \\\n' % (sub[2], sub[1], title, sub[0], sub[1]) 473 | cmd = cmd[:-2] 474 | f.write(cmd) 475 | return merge_script 476 | 477 | 478 | def test_crf(name, codec, crf_begin, target, step=1, old: dict = None, gap: int = 250): 479 | cwd = os.getcwd() 480 | bframes = 16 481 | if codec == "x265": 482 | bframes = 6 483 | 484 | if "{}.log".format(name) not in os.listdir(): 485 | os.system("rm -rf crf") 486 | os.chdir(vstest_path) 487 | os.system("python3 vstest.py -codec {} -extract 85:2000:10000 --crf [{}/{}/{}] --bframes {}".format(codec, 488 | crf_begin, 489 | crf_begin, 490 | step, 491 | bframes)) 492 | os.chdir(cwd) 493 | os.system("rm -rf crf") 494 | os.system("mv {}.log {}.log".format(codec, name)) 495 | with open("{}.log".format(name)) as f: 496 | content = f.read() 497 | crf = list(map(lambda x: float(x), re.findall(r"--crf (.+?) ", content))) 498 | bit_rate = list(map(lambda x: float(x), re.findall(r"encoded.+, (.+?) kb", content))) 499 | if abs(bit_rate[0] - target) <= gap: 500 | return crf[0] 501 | 502 | if old: 503 | crf += old['crf'] 504 | bit_rate += old['bit_rate'] 505 | crf.sort() 506 | bit_rate.sort() 507 | bit_rate.reverse() 508 | if bit_rate[0] < target: 509 | os.system("rm {}.log".format(name)) 510 | return test_crf(name, codec, crf_begin - step, target, step, old={'crf': crf, 'bit_rate': bit_rate}) 511 | if bit_rate[-1] >= target: 512 | os.system("rm {}.log".format(name)) 513 | return test_crf(name, codec, crf_begin + step, target, step, old={'crf': crf, 'bit_rate': bit_rate}) 514 | num = 0 515 | for i in range(len(bit_rate)): 516 | if float(bit_rate[i]) < target: 517 | num = i 518 | break 519 | 520 | print(name, bit_rate, crf) 521 | f1 = np.polyfit(np.array(bit_rate), np.array(crf), 3) 522 | p1 = np.poly1d(f1) 523 | target_crf = p1(target) 524 | if target_crf > crf[i] or target_crf < crf[i - 1]: 525 | target_crf = (crf[i] + crf[i - 1]) / 2 526 | 527 | target_crf = format(target_crf, '.1f') 528 | return target_crf 529 | 530 | 531 | def detect_crf(name, codec, target): 532 | cwd = os.getcwd() 533 | os.chdir(vstest_path) 534 | with open("vstestconfig.py.template") as f: 535 | filename = "{}-{}.vpy".format(title, name) 536 | vstestconfig = f.read() 537 | vstestconfig = vstestconfig.replace("REPLACE_SCRIPT", "{}/{}".format(output_dir, filename)) 538 | vstestconfig = vstestconfig.replace("REPLACE_FOLDER", "{}/".format(output_dir)) 539 | with open("vstestconfig.py", "w") as ff: 540 | ff.write(vstestconfig) 541 | os.chdir(cwd) 542 | step = 1 543 | crf_begin = 17 544 | crf_range = 3 545 | if name == "x265": 546 | crf_begin = 18 547 | crf_range = 4 548 | return test_crf(name, codec, crf_begin, target, step) 549 | 550 | 551 | def generate_vpy_helper(name, content, target): 552 | codec = "x265" 553 | if name != "x265": 554 | codec = "x264" 555 | content = content.replace("REPLACE_PRESET", name) 556 | content = content.replace("encode_x264 = False", "encode_x264 = True") 557 | filename = "{}-{}.vpy".format(title, name) 558 | with open(filename, "w") as f: 559 | f.write(content) 560 | return detect_crf(name, codec, target) 561 | 562 | 563 | def generate_vpy(conf): 564 | if f"{title}.ipynb" not in os.listdir("/scripts/"): 565 | with open("/scripts/" + title + ".ipynb", "w") as f: 566 | f.write(ipy.replace("REPLACE_PATH", "{}/{}_{}.264".format(output_dir, title, video_track))) 567 | input("请修改jupyter中的{}.ipynb文件,确定切边条数,完成后按任意键继续".format(title)) 568 | with open("/scripts/" + title + ".ipynb") as f: 569 | py = list(map(lambda x: x.strip('\n'), json.loads(f.read())['cells'][1]['source'][1:])) 570 | content = "\n".join(py) 571 | with open(title + ".vpy", "w") as f: 572 | f.write(content) 573 | crf_dict = {} 574 | for k, v in conf.items(): 575 | if v != 0: 576 | crf_dict[k] = generate_vpy_helper(k, content, v) 577 | return crf_dict 578 | 579 | 580 | def get_frames(): 581 | frames = "" 582 | frame_cmd = 'vspipe --y4m "%s/%s.vpy" -i -' % (output_dir, title) 583 | tmp_res = os.popen(frame_cmd).read().split("\n") 584 | for tmpl in tmp_res: 585 | if tmpl.startswith("Frames"): 586 | frames = " --frames " + tmpl.split()[1] 587 | return frames 588 | 589 | 590 | def has_suffix_in_path(suffix, path=None): 591 | if (path): 592 | files = os.listdir(path) 593 | else: 594 | files = os.listdir() 595 | for i in range(len(files)): 596 | files[i] = os.path.splittext(files[i])[1] 597 | 598 | if suffix in files: 599 | return True 600 | return False 601 | 602 | 603 | def main(): 604 | ts = time.time() 605 | encoder = [] 606 | 607 | # demux and generate the scripts 608 | os.chdir(os.path.dirname(path)) 609 | 610 | while not init_done: 611 | init(path) 612 | if "{}.ipynb".format(title) not in os.listdir("/scripts"): 613 | demux(path) 614 | move_files(os.getcwd(), output_dir) 615 | os.chdir(output_dir) 616 | crf_dict = generate_vpy(conf) 617 | frames = get_frames() 618 | 619 | for k, v in conf.items(): 620 | if v == 0: 621 | continue 622 | if k == "x265": 623 | # 265 624 | encode_tool = "x265" 625 | demuxer = "--y4m" 626 | encode_parameter = x265_parameter 627 | suffix = "hevc" 628 | else: 629 | # 264 630 | encode_tool = "x264" 631 | demuxer = "--demuxer y4m" 632 | encode_parameter = x264_parameter 633 | suffix = "avc" 634 | 635 | encode_script = "{}/{}-{}.sh".format(output_dir, title, k) 636 | with open(encode_script, "w") as f: 637 | f.write('vspipe --y4m "%s/%s-%s.vpy" - | %s %s --output "%s/%s-%s.%s" - --crf %s %s' % ( 638 | output_dir, title, k, encode_tool, demuxer, output_dir, title, k, suffix, crf_dict[k], 639 | encode_parameter)) 640 | if frames: 641 | f.write(frames) 642 | merge_script = generate_merge_script(k) 643 | encoder.append(Encoder(encode_script, merge_script)) 644 | 645 | for it in encoder: 646 | it.encode() 647 | it.merge() 648 | 649 | os.system("mkdir -p {}/torrent".format(output_dir)) 650 | for i in os.listdir(output_dir): 651 | if i.endswith(".mkv"): 652 | os.system("mediainfo {} > {}/torrent/{}.txt".format(i, output_dir, i)) 653 | os.system("mktorrent -a \"x\" -l 22 -p {}".format(i)) 654 | os.system("mv *.torrent torrent") 655 | 656 | # os.system("rm -f {}/*.vpy {}/*.sh".format(output_dir, output_dir)) 657 | # print("rm -f {}/*.vpy {}/*.sh".format(output_dir, output_dir)) 658 | # print("rm -f /scripts/{}.vpy".format(title)) 659 | te = time.time() 660 | print("压制完成,耗时{},请查看{}目录".format(format_time(te - ts), output_dir)) 661 | 662 | 663 | if __name__ == "__main__": 664 | main() 665 | -------------------------------------------------------------------------------- /scripts/jupyter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | jupyter notebook --allow-root 3 | --------------------------------------------------------------------------------