├── .gitignore ├── Gruntfile.js ├── README.md ├── app.js ├── config ├── default.json ├── rtx-2070-super.json └── rtx-2080-ti.json ├── docs └── waifu2x-caffe-cui.md ├── image-comparisons ├── sample1.png ├── sample2.png ├── sample3.png ├── sample4.png ├── sample5.png └── sample6.png ├── lib ├── console-utils.js └── utils.js ├── package.json ├── template └── version.tpl └── test-vids ├── 10-second-sample-Prince-of-Tennis.mkv ├── 20-second-sample-Aishiteru-ze-Baby.mkv └── 30-second-sample-Inuyasha.mkv /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | package.json 90 | package-lock.json 91 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // project configuration 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | 7 | build_dir: 'build_tmp', 8 | archive_dir: 'build', 9 | template_dir: 'template', 10 | 11 | clean: { 12 | build_dir: '<%= build_dir %>', 13 | build_exe: '<%= pkg.name %>.exe' 14 | }, 15 | 16 | exec: { package_source: { cmd: 'pkg .' } }, 17 | 18 | create_dir: { 19 | build_dir: ['<%= build_dir %>'] 20 | }, 21 | 22 | textfile: { 23 | options: { dest: '<%= build_dir %>' }, 24 | version: { 25 | options: { 26 | template: 'version.tpl', 27 | templateDir: '<%= template_dir %>', 28 | urlFormat: 'version.txt', 29 | customVars: { 30 | name: '<%= pkg.name %>', 31 | version: '<%= pkg.version %>', 32 | description: '<%= pkg.description %>', 33 | link: '<%= pkg.homepage %>' 34 | } 35 | } 36 | } 37 | }, 38 | 39 | move: { 40 | build_exe: { 41 | src: '<%= pkg.name %>.exe', 42 | dest: '<%= build_dir %>/<%= pkg.name %>.exe' 43 | } 44 | }, 45 | 46 | copy: { 47 | main: { 48 | files: [ 49 | // include default config file 50 | { expand: true, src: ['config/default.json', 'config/rtx-2080-ti.json'], dest: '<%= build_dir %>/', filter: 'isFile' }, 51 | 52 | // include test sample videos 53 | { expand: true, src: ['testvids/*'], dest: '<%= build_dir %>/', filter: 'isFile' } 54 | ] 55 | } 56 | }, 57 | 58 | compress: { 59 | build: { 60 | options: { archive: '<%= archive_dir %>/<%= pkg.name %>(v<%= pkg.version %>).zip' }, 61 | files: [{ expand: true, cwd: '<%= build_dir %>/', src: ['**/*'], dest: '/' }] 62 | } 63 | }, 64 | 65 | }) 66 | 67 | grunt.loadNpmTasks('grunt-contrib-clean'); 68 | grunt.loadNpmTasks('grunt-contrib-copy'); 69 | grunt.loadNpmTasks('grunt-contrib-compress'); 70 | grunt.loadNpmTasks('grunt-exec'); 71 | grunt.loadNpmTasks('grunt-move'); 72 | grunt.loadNpmTasks('grunt-textfile'); 73 | 74 | grunt.registerTask('create_dir', 'creates folder(s)', function() { 75 | var taskName = this.name; 76 | this.args.forEach(function(arg) { 77 | var val = grunt.config.get([taskName, arg].join('.')); 78 | if(Array.isArray(val)) { 79 | val.forEach(function(dirName) { 80 | grunt.file.mkdir(dirName); 81 | }) 82 | } else { 83 | grunt.file.mkdir(val); 84 | } 85 | }); 86 | }); 87 | 88 | grunt.registerTask('default', 'packages node source code into a packaged zip containing an exe', function() { 89 | grunt.task.run([ 90 | 'clean:build_dir:build_exe', 91 | 'exec:package_source', 92 | 'create_dir:build_dir', 93 | 'textfile:version', 94 | 'move:build_exe', 95 | 'copy:main', 96 | 'compress:build', 97 | 'clean:build_dir:build_exe', 98 | ]); 99 | }); 100 | }; 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video2x 2 | This tool upscales smaller resolution videoes to a higher resolution based on the [Waifu2x](https://github.com/nagadomi/waifu2x) art upscaler algorithm. It is based off of an [existing python project](https://github.com/K4YT3X/video2x) using the same core principles to perform the video upscale, but with additional functionality to provide less verbouse output and hopefully more meaningful output. It's built to be flexible in terms of what options you pass into either the video encoder (ffmpeg) or the upscaler (waifu2x). 3 | 4 | This project is in github! You can find the page [here](https://github.com/kwsou/video2x). You can also grab the latest release [here](https://github.com/kwsou/video2x/releases). 5 | 6 | ## How it works 7 | 1. Extract every frame in the source video. 8 | 2. Use waifu2x to upscale the frame to a higher resolution. Many threads of this is run in parallel to split the work. 9 | 3. Package the upscaled frames back into a new video, while copying over any audio, subtitle, and attachement streams (if applicable) from the source video. 10 | 11 | This is a very process intensive task, so expect to take quite a while (and enough disk space). 12 | 13 | ## Image comparisons 14 | Visit this [link](https://video2x.kwsou.com/image-comparison/) to see my compiled list of screenshot comparisons. 15 | 16 | ## Requirements 17 | 1. [FFmpeg](https://www.ffmpeg.org/). Add the path to the ffmpeg executable into your PATH environment. [Here's instructions on how to do this](https://github.com/adaptlearning/adapt_authoring/wiki/Installing-FFmpeg) 18 | 2. Windows executable of the waifu2x tool, [waifu2x-caffe](https://github.com/lltcggie/waifu2x-caffe). 19 | 3. [Nodejs](https://nodejs.org/en/) (Optional, only needed if you want to build from source) 20 | 21 | ## Optional 22 | 1. I highly recommend the [NVIDIA CUDA Deep Neural Network](https://developer.nvidia.com/cudnn) (cuDNN) library when using waifu2x-caffe. This is a high-speed machine learning library that **can only be used with NVIDIA GPUs**. Compared to using your cpu or gpu (via CUDA) to upscale images, cuDNN offers the following advantages: 23 | 24 | - Depending on the type of GPU used, images can be converted faster (exponentially faster in my experience) 25 | - VRAM usage can be reduced 26 | 27 | Due to licensing issues, waifu2x-caffe does not include this library by default. Here are instructions on how to obtain this library and have waifu2x-caffe use it: 28 | 29 | - Visit the link above to download the cuDNN binary for Windows x64. (You would need to register as a developer in order to download first). 30 | - Prevent any potential permissions issue by unblocking the downloaded binary zip 31 | - Copy `cuda/bin/cudnn64_7.dll` to where you put waifu2x-caffe. 32 | - Run `waifu2x-caffe.exe` and click on the `Check cuDNN` button to ensure that waifu2x can use the cuDNN library. 33 | 34 | 35 | ## Configuration 36 | You can find configuration files under `config`. The format of the JSON structure is as follows: 37 | ``` 38 | { 39 | // workspace folder to temporarily store frames into 40 | "workDirectory": "temp", 41 | 42 | // new upscaled width and height 43 | "width": 1920, 44 | "height": 1080, 45 | 46 | // ffmpeg video encoding (repack to new video) settings 47 | "ffmpeg": { 48 | 49 | // number of threads ffmpeg will run on, 0 defaults to having it use all of your cpu 50 | // recommended to run #cpu cores - 1 or 2 to avoid having cpu usage spike up to 100% 51 | "numThreads": 2, 52 | 53 | // if you want to use a different encoding library, change this 54 | "encoderLib": "libx264", 55 | 56 | // as well as providing the option flag you need in addition below 57 | "encodingOptions": { 58 | "libx264": [ 59 | ... 60 | ] 61 | } 62 | }, 63 | 64 | // waifu2x upscaler options 65 | "waifu2x": { 66 | 67 | // full path of waifu2x-caffe executable 68 | "directory": "C:/dev/tools/waifu2x-caffe", 69 | 70 | // the model to use. This is a folder path from the base directory above 71 | "model": "models/anime_style_art", 72 | 73 | // you can run waifu2x-caffe-cui in CPU, GPU, or CUDNN mode, this contains common options for all modes 74 | "COMMON_PRESET": [ 75 | ... 76 | ], 77 | 78 | // specific mode options 79 | "CPU_PRESET": [ ... ], 80 | "GPU_PRESET": [ ... ], 81 | "CUDNN_PRESET": [ ... ], 82 | 83 | // Work distribution. You can define as many threads as your PC can support. (ex. for 4 CPU cores + 1 gpu CUDA) 84 | "threads": [ 85 | { 86 | "type": "gpu", 87 | // the percentage of frames this thread will process (these should sum up to 1.00) 88 | "weight": 0.5, 89 | "preset": "GPU_PRESET" 90 | }, 91 | { 92 | "type": "cpu", 93 | "weight": 0.125, 94 | "preset": "CPU_PRESET" 95 | }, 96 | { 97 | "type": "cpu", 98 | "weight": 0.125, 99 | "preset": "CPU_PRESET" 100 | }, 101 | { 102 | "type": "cpu", 103 | "weight": 0.125, 104 | "preset": "CPU_PRESET" 105 | }, 106 | { 107 | "type": "cpu", 108 | "weight": 0.125, 109 | "preset": "CPU_PRESET" 110 | } 111 | ] 112 | } 113 | } 114 | ``` 115 | I encourage you to modify the settings to suit your own needs based on your image perferences and workload distribution. I've included some short sample videos under `test-vids`. You can look at other available [ffmpeg video encoders](https://www.ffmpeg.org/ffmpeg-codecs.html#Video-Encoders) and see available [waifu2x-caffe-cui options](https://github.com/kwsou/video2x/blob/master/docs/waifu2x-caffe-cui.md). 116 | 117 | For reference, I've included my configuration in `config/rtx-2080-ti.json`. Using the cuDNN library, only one cudnn thread is used as it is faster than the other modes. 118 | 119 | ## Running the executable 120 | * Open a command prompt and `cd` where the executable is located 121 | * enter `video2x.exe -i INPUT -o OUTPUT -c CONFIG`, where `INPUT` is the video source file, `OUTPUT` is the name of the new video, and `CONFIG` points to a valid config JSON file (see above). For example, 122 | ``` 123 | video2x.exe -i "C:\videos\video1.mp4" -o "C:\videos\new_video1.mp4" -c "config\default.json" 124 | ``` 125 | 126 | Sample output 127 | === 128 | ``` 129 | C:\dev\video2x\build\video2x(v1.0.0)>video2x.exe -i testvids\30-second-sample-Inuyasha.mkv -c config\custom.json 130 | # # # ##### ###### #### ##### # # 131 | # # # # # # # # # # # # 132 | # # # # # ##### # # # ## 133 | # # # # # # # # ##### ## 134 | # # # # # # # # # # # 135 | ## # ##### ###### #### ####### # # 136 | 137 | Reading config file in "config\custom.json" 138 | Retrieving video metadata 139 | Command: ffprobe -of json -show_streams -show_format "testvids\30-second-sample-Inuyasha.mkv" 140 | Creating new workspace directory: D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf 141 | Creating directory under workspace to store extracted frames: D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\extracted 142 | Creating directory under workspace to store upscaled frames: D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\upscaled 143 | Extracting frames from source video 144 | Command: ffmpeg -i testvids\30-second-sample-Inuyasha.mkv -y D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\extracted\frame%d.png 145 | Extracted 718 frames from "testvids\30-second-sample-Inuyasha.mkv" (Size: 0.28 GB, Time: 0.00 hours, approx. 0 minutes) 146 | Upscaling the extracted frames from 640x480 to 2560x1440 147 | Splitting up the frames into smaller groups, each to be processed by its own waifu2x thread 148 | [Thread1] 359 (50.0%) frames - gpu mode 149 | Command: C:/dev/waifu2x-caffe/waifu2x-caffe-cui.exe -i D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\extracted\thread1 -o D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\upscaled -w 2560 -h 1440 -p gpu --gpu 0 -b 15 -c 256 -e png -m noise_scale -n 1 150 | [Thread2] 89 (12.5%) frames - cpu mode 151 | Command: C:/dev/waifu2x-caffe/waifu2x-caffe-cui.exe -i D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\extracted\thread2 -o D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\upscaled -w 2560 -h 1440 -e png -m noise_scale -n 1 152 | [Thread3] 89 (12.5%) frames - cpu mode 153 | Command: C:/dev/waifu2x-caffe/waifu2x-caffe-cui.exe -i D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\extracted\thread3 -o D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\upscaled -w 2560 -h 1440 -e png -m noise_scale -n 1 154 | [Thread4] 89 (12.5%) frames - cpu mode 155 | Command: C:/dev/waifu2x-caffe/waifu2x-caffe-cui.exe -i D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\extracted\thread4 -o D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\upscaled -w 2560 -h 1440 -e png -m noise_scale -n 1 156 | [Thread5] 92 (12.8%) frames - cpu mode 157 | Command: C:/dev/waifu2x-caffe/waifu2x-caffe-cui.exe -i D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\extracted\thread5 -o D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\upscaled -w 2560 -h 1440 -e png -m noise_scale -n 1 158 | 159 | [Thread4] Completed in 0.07 hours (approx. 4 minutes) 160 | [Thread3] Completed in 0.07 hours (approx. 4 minutes) 161 | [Thread2] Completed in 0.07 hours (approx. 4 minutes) 162 | [Thread5] Completed in 0.07 hours (approx. 4 minutes) 163 | [Thread1] Completed in 0.08 hours (approx. 5 minutes) 164 | All waifu2x threads successfully finished. Upscaled 718 frames (Size: 1.80 GB, 6.38x the original size) 165 | Converting upscaled frames to new video 166 | Command: ffmpeg -r 24000/1001 -f image2 -i D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf\upscaled\frame%d.png -i testvids\30-second-sample-Inuyasha.mkv -y -map_metadata 1 -map 0:v -vcodec libx264 -preset slow -crf 18 -pix_fmt yuv420p -map 1:a -acodec copy -map 1:s? -map 1:t? 30-second-sample-Inuyasha_new.mkv 167 | New video created "30-second-sample-Inuyasha_new.mkv" (Size: 0.04 GB, 4.23x the original size, Time: 0.02 hours, approx. 1 minutes) 168 | Cleaning up workspace directory: D:\test\3ca00a20-dd9f-11e8-af5d-e906be9beccf 169 | Video2x successfully finished all tasks in 0.10 hours 170 | ``` 171 | 172 | Building from source (Optional) 173 | === 174 | If you don't care about modifying the code and/or building your own executable from the source, skip the section below. 175 | ## Setup (dev) 176 | 177 | * Obtain a copy of the source 178 | * Open a terminal and `cd` into the source 179 | * Run `npm install` to obtain node dependencies 180 | * To run, enter `node app.js -i INPUT -o OUTPUT -c CONFIG`, where `INPUT` is the video source file, `OUTPUT` is the name of the new video, and `CONFIG` points to a valid config JSON file (see above). 181 | 182 | ## Build (dev) 183 | If you want to compile the source yourself to form a standalone executable, do the following: 184 | 185 | * Install pkg by running `npm install -g pkg` 186 | * Install grunt by running `npm install -g grunt` 187 | * Grunt tasks are setup already without needing you to configure anything. Build the project by running `grunt` 188 | * Once the task finishes, you can find the packaged project under `.\build` 189 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const argv = require('minimist')(process.argv.slice(2)), 2 | fs = require('fs-extra'), 3 | path = require('path'), 4 | Q = require('q'), 5 | printf = require('printf'), 6 | uuidv1 = require('uuid/v1'); 7 | 8 | const cutils = require('./lib/console-utils'), 9 | utils = require('./lib/utils'); 10 | 11 | var showBanner = function() { 12 | console.log(' # # # ##### ###### #### ##### # # '); 13 | console.log(' # # # # # # # # # # # # '); 14 | console.log(' # # # # # ##### # # # ## '); 15 | console.log(' # # # # # # # # ##### ## '); 16 | console.log(' # # # # # # # # # # # '); 17 | console.log(' ## # ##### ###### #### ####### # # '); 18 | console.log(' '); 19 | }; 20 | 21 | var getRequiredArgument = function(argName) { 22 | if(argv[argName]) { 23 | return argv[argName]; 24 | } else { 25 | console.log(printf('Error: missing argument "%s"', argName)); 26 | process.exit(-1); 27 | } 28 | }; 29 | 30 | var initConfig = function() { 31 | var deferred = Q.defer(); 32 | var configPath = argv.c ? argv.c : './config/default.json'; 33 | 34 | utils.performStep(printf('Reading config file in "%s"', configPath), fs.readJson, [configPath]).then(function(config) { 35 | // attach argument values into config 36 | config.input = getRequiredArgument('i'); 37 | config.output = argv.o ? argv.o : null; 38 | if(!config.output) { 39 | const FILE_EXTENSION = path.extname(config.input); 40 | config.output = printf('%s_new%s', path.basename(config.input, FILE_EXTENSION), FILE_EXTENSION); 41 | } 42 | 43 | deferred.resolve(config); 44 | }, cutils.showError); 45 | return deferred.promise; 46 | }; 47 | 48 | var start = function(config) { 49 | // ensure workspace directory is a completly new directory 50 | const WORK_DIRECTORY = path.join(config.workDirectory, uuidv1()), 51 | EXTRACTED_FRAMES_DIRECTORY = path.join(WORK_DIRECTORY, 'extracted'), 52 | UPSCALED_FRAMES_DIRECTORY = path.join(WORK_DIRECTORY, 'upscaled'), 53 | FRAME_FILE_PATTERN = 'frame%d.png'; 54 | 55 | var startTime = Date.now(); 56 | var metadata, originalFrameSize, upscaledFrameSize; 57 | utils.performStep('Retrieving video metadata', function() { 58 | return utils.getVideoMetadata(config.input); 59 | }).then(function(data) { 60 | metadata = data; 61 | return utils.performStep(printf('Creating new workspace directory: %s', WORK_DIRECTORY), fs.emptyDir, [WORK_DIRECTORY]); 62 | }).then(function() { 63 | return utils.performStep(printf('Creating directory under workspace to store extracted frames: %s', EXTRACTED_FRAMES_DIRECTORY), fs.emptyDir, [EXTRACTED_FRAMES_DIRECTORY]); 64 | }).then(function() { 65 | return utils.performStep(printf('Creating directory under workspace to store upscaled frames: %s', UPSCALED_FRAMES_DIRECTORY), fs.emptyDir, [UPSCALED_FRAMES_DIRECTORY]); 66 | }).then(function() { 67 | return utils.performStep('Extracting frames from source video', function() { 68 | var d = Q.defer(); 69 | 70 | var startTime = Date.now(); 71 | utils.extractPNGFromSourceVideo(config.ffmpeg, config.input, EXTRACTED_FRAMES_DIRECTORY, FRAME_FILE_PATTERN).then(function() { 72 | // show statistics 73 | utils.getDirectorySize(EXTRACTED_FRAMES_DIRECTORY).then(function(dirInfo) { 74 | originalFrameSize = dirInfo.totalSize / (1024 * 1024 * 1024); 75 | cutils.stepComment(printf('Extracted %i frames from "%s" (Size: %0.2f GB, Time: %0.2f hours, approx. %0.0f minutes)', dirInfo.files.length, config.input, originalFrameSize, utils.timeDiffHrs(startTime), utils.timeDiffMins(startTime))); 76 | d.resolve(dirInfo.files); 77 | }, d.reject); 78 | }, d.reject); 79 | 80 | return d.promise; 81 | }); 82 | }).then(function(extractedFrames) { 83 | var oldResolution = printf('%ix%i', metadata.stream.width, metadata.stream.height); 84 | var newResolution = printf('%ix%i', config.width, config.height); 85 | return utils.performStep(printf('Upscaling the extracted frames from %s to %s', oldResolution, newResolution), function() { 86 | var d = Q.defer(); 87 | 88 | utils.performStep('Splitting up the frames into smaller groups, each to be processed by its own waifu2x thread', function() { 89 | var startTime = Date.now(); 90 | var waifu2xThreads = [], currFrameIndex = 0, totalFrames = extractedFrames.length; 91 | for(var i = 0; i < config.waifu2x.threads.length; i++) { 92 | var currThread = config.waifu2x.threads[i]; 93 | var threadNum = i + 1; 94 | var totalFramesHandled = Math.floor(totalFrames * currThread.weight); 95 | 96 | if(i < config.waifu2x.threads.length - 1) { 97 | cutils.stepComment(printf('[Thread%i] %i (%.1f%) frames - %s mode', threadNum, totalFramesHandled, currThread.weight * 100, currThread.type)); 98 | } else { 99 | totalFramesHandled = totalFrames - currFrameIndex; 100 | cutils.stepComment(printf('[Thread%i] %i (%.1f%) frames - %s mode', threadNum, totalFramesHandled, (totalFramesHandled / totalFrames) * 100, currThread.type)); 101 | } 102 | 103 | var currThreadWorkspace = path.join(EXTRACTED_FRAMES_DIRECTORY, 'thread' + threadNum); 104 | fs.emptyDirSync(currThreadWorkspace); 105 | for(var k = 0; k < totalFramesHandled; k++) { 106 | var frameIndex = currFrameIndex + k; 107 | fs.moveSync(path.join(EXTRACTED_FRAMES_DIRECTORY, extractedFrames[frameIndex]), path.join(currThreadWorkspace, extractedFrames[frameIndex])); 108 | } 109 | currFrameIndex += totalFramesHandled; 110 | 111 | var newWaifu2xThread = utils.spawnWaifu2x(threadNum, config.waifu2x.directory, config.waifu2x[currThread.preset].concat(config.waifu2x.COMMON_PRESET), config.width, config.height, currThreadWorkspace, UPSCALED_FRAMES_DIRECTORY, config.waifu2x.model); 112 | newWaifu2xThread.done(function(threadId) { 113 | cutils.stepComment(printf('[Thread%i] Completed in %0.2f hours (approx. %0.0f minutes)', threadId, utils.timeDiffHrs(startTime), utils.timeDiffMins(startTime))); 114 | }); 115 | waifu2xThreads.push(newWaifu2xThread); 116 | } 117 | 118 | // just add a line break to make it easier to see 119 | cutils.stepComment(''); 120 | return Q.allSettled(waifu2xThreads); 121 | }).then(function() { 122 | // show statistics 123 | utils.getDirectorySize(UPSCALED_FRAMES_DIRECTORY).then(function(dirInfo) { 124 | upscaledFrameSize = dirInfo.totalSize / (1024 * 1024 * 1024); 125 | cutils.stepComment(printf('All waifu2x threads successfully finished. Upscaled %i frames (Size: %0.2f GB, %0.2fx the original size)', dirInfo.files.length, upscaledFrameSize, upscaledFrameSize / originalFrameSize)); 126 | d.resolve(); 127 | }), d.reject; 128 | }); 129 | 130 | return d.promise; 131 | }); 132 | }).then(function() { 133 | return utils.performStep('Converting upscaled frames to new video', function() { 134 | var d = Q.defer(); 135 | 136 | var startTime = Date.now(); 137 | utils.combinePNGToNewVideo(metadata, config.ffmpeg, config.input, path.join(UPSCALED_FRAMES_DIRECTORY, FRAME_FILE_PATTERN), config.output).then(function() { 138 | var originalVideoSize = (fs.statSync(config.input).size) / (1024 * 1024 * 1024); 139 | var newVideoSize = (fs.statSync(config.output).size) / (1024 * 1024 * 1024); 140 | cutils.stepComment(printf('New video created "%s" (Size: %0.2f GB, %0.2fx the original size, Time: %0.2f hours, approx. %0.0f minutes)', config.output, newVideoSize, newVideoSize / originalVideoSize, utils.timeDiffHrs(startTime), utils.timeDiffMins(startTime))); 141 | d.resolve(); 142 | }, d.reject); 143 | 144 | return d.promise; 145 | }); 146 | }).then(function() { 147 | return utils.performStep(printf('Cleaning up workspace directory: %s', WORK_DIRECTORY), fs.remove, [WORK_DIRECTORY]); 148 | }).then(function() { 149 | cutils.stepComment(printf('Video2x successfully finished all tasks in %0.2f hours', utils.timeDiffHrs(startTime))); 150 | process.exit(0); 151 | }).catch(cutils.showError); 152 | }; 153 | 154 | // start 155 | showBanner(); 156 | initConfig().done(function(config) { 157 | start(config); 158 | }); -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "workDirectory": "temp", 3 | "width": 1920, 4 | "height": 1080, 5 | "ffmpeg": { 6 | "numThreads": 0, 7 | "encoderLib": "libx264", 8 | "encodingOptions": { 9 | "libx264": [ 10 | { 11 | "flag": "-preset", 12 | "value": "slow" 13 | }, 14 | { 15 | "flag": "-crf", 16 | "value": 22 17 | }, 18 | { 19 | "flag": "-pix_fmt", 20 | "value": "yuv420p" 21 | } 22 | ] 23 | } 24 | }, 25 | "waifu2x": { 26 | "directory": "C:/dev/tools/waifu2x-caffe", 27 | "model": "models/anime_style_art", 28 | "COMMON_PRESET": [ 29 | { 30 | "flag": "-e", 31 | "value": "png" 32 | }, 33 | { 34 | "flag": "-m", 35 | "value": "noise_scale" 36 | }, 37 | { 38 | "flag": "-n", 39 | "value": 1 40 | }, 41 | { 42 | "flag": "-t", 43 | "value": 0 44 | } 45 | ], 46 | "CPU_PRESET": [ 47 | { 48 | "flag": "-p", 49 | "value": "cpu" 50 | } 51 | ], 52 | "GPU_PRESET": [ 53 | { 54 | "flag": "-p", 55 | "value": "gpu" 56 | }, 57 | { 58 | "flag": "--gpu", 59 | "value": 0 60 | }, 61 | { 62 | "flag": "-b", 63 | "value": 1 64 | }, 65 | { 66 | "flag": "-c", 67 | "value": 8 68 | } 69 | ], 70 | "threads": [ 71 | { 72 | "type": "cpu", 73 | "weight": 0.5, 74 | "preset": "CPU_PRESET" 75 | }, 76 | { 77 | "type": "gpu", 78 | "weight": 0.5, 79 | "preset": "GPU_PRESET" 80 | } 81 | ] 82 | } 83 | } -------------------------------------------------------------------------------- /config/rtx-2070-super.json: -------------------------------------------------------------------------------- 1 | { 2 | "workDirectory": "temp", 3 | "width": 1920, 4 | "height": 1440, 5 | "ffmpeg": { 6 | "numThreads": 0, 7 | "encoderLib": "libx264", 8 | "encodingOptions": { 9 | "libx264": [ 10 | { 11 | "flag": "-preset", 12 | "value": "slow" 13 | }, 14 | { 15 | "flag": "-crf", 16 | "value": 19 17 | }, 18 | { 19 | "flag": "-pix_fmt", 20 | "value": "yuv420p" 21 | } 22 | ] 23 | } 24 | }, 25 | "waifu2x": { 26 | "directory": "C:/dev/tools/waifu2x-caffe", 27 | "model": "models/cunet", 28 | "COMMON_PRESET": [ 29 | { 30 | "flag": "-e", 31 | "value": "png" 32 | }, 33 | { 34 | "flag": "-m", 35 | "value": "noise_scale" 36 | }, 37 | { 38 | "flag": "-n", 39 | "value": 2 40 | }, 41 | { 42 | "flag": "-t", 43 | "value": 0 44 | } 45 | ], 46 | "CUDNN_PRESET": [ 47 | { 48 | "flag": "-p", 49 | "value": "cudnn" 50 | }, 51 | { 52 | "flag": "--gpu", 53 | "value": 0 54 | }, 55 | { 56 | "flag": "-b", 57 | "value": 8 58 | }, 59 | { 60 | "flag": "-c", 61 | "value": 92 62 | } 63 | ], 64 | "threads": [ 65 | { 66 | "type": "cuDNN", 67 | "weight": 1, 68 | "preset": "CUDNN_PRESET" 69 | } 70 | ] 71 | } 72 | } -------------------------------------------------------------------------------- /config/rtx-2080-ti.json: -------------------------------------------------------------------------------- 1 | { 2 | "workDirectory": "temp", 3 | "width": 1920, 4 | "height": 1440, 5 | "ffmpeg": { 6 | "numThreads": 0, 7 | "encoderLib": "libx264", 8 | "encodingOptions": { 9 | "libx264": [ 10 | { 11 | "flag": "-preset", 12 | "value": "slow" 13 | }, 14 | { 15 | "flag": "-crf", 16 | "value": 19 17 | }, 18 | { 19 | "flag": "-pix_fmt", 20 | "value": "yuv420p" 21 | } 22 | ] 23 | } 24 | }, 25 | "waifu2x": { 26 | "directory": "C:/dev/tools/waifu2x-caffe", 27 | "model": "models/anime_style_art", 28 | "COMMON_PRESET": [ 29 | { 30 | "flag": "-e", 31 | "value": "png" 32 | }, 33 | { 34 | "flag": "-m", 35 | "value": "noise_scale" 36 | }, 37 | { 38 | "flag": "-n", 39 | "value": 2 40 | }, 41 | { 42 | "flag": "-t", 43 | "value": 0 44 | } 45 | ], 46 | "CUDNN_PRESET": [ 47 | { 48 | "flag": "-p", 49 | "value": "cudnn" 50 | }, 51 | { 52 | "flag": "--gpu", 53 | "value": 0 54 | }, 55 | { 56 | "flag": "-b", 57 | "value": 32 58 | }, 59 | { 60 | "flag": "-c", 61 | "value": 128 62 | } 63 | ], 64 | "threads": [ 65 | { 66 | "type": "cuDNN", 67 | "weight": 1, 68 | "preset": "CUDNN_PRESET" 69 | } 70 | ] 71 | } 72 | } -------------------------------------------------------------------------------- /docs/waifu2x-caffe-cui.md: -------------------------------------------------------------------------------- 1 | # Waifu2x-Caffe-CUI Options 2 | 3 | Haven't really found a resource explaining the various options you can pass into waifu2x-caffe (in english) so I am leaving this list here for reference. You can look at the help command if you run `waifu2x-caffe-cui.exe --help`. 4 | 5 | This output is based on [waifu2x-caffe](https://github.com/lltcggie/waifu2x-caffe) v1.1.9.2: 6 | 7 | ``` 8 | USAGE: 9 | 10 | waifu2x-caffe-cui.exe [-t <0|1>] [--gpu ] [-b ] [--crop_h 11 | ] [--crop_w ] [-c ] [-d ] [-q 12 | ] [-p ] [--model_dir 13 | ] [-h ] [-w ] [-s 14 | ] [-n <0|1|2|3>] [-m ] [-e ] [-l 16 | ] [-o ] -i [--] 17 | [--version] [-?] 18 | 19 | 20 | Where: 21 | 22 | -t <0|1>, --tta <0|1> 23 | 8x slower and slightly high quality 24 | 25 | --gpu 26 | gpu device no 27 | 28 | -b , --batch_size 29 | input batch size 30 | 31 | --crop_h 32 | input image split size(height) 33 | 34 | --crop_w 35 | input image split size(width) 36 | 37 | -c , --crop_size 38 | input image split size 39 | 40 | -d , --output_depth 41 | output image chaneel depth bit 42 | 43 | -q , --output_quality 44 | output image quality 45 | 46 | -p , --process 47 | process mode 48 | 49 | --model_dir 50 | path to custom model directory (don't append last / ) 51 | 52 | -h , --scale_height 53 | custom scale height 54 | 55 | -w , --scale_width 56 | custom scale width 57 | 58 | -s , --scale_ratio 59 | custom scale ratio 60 | 61 | -n <0|1|2|3>, --noise_level <0|1|2|3> 62 | noise reduction level 63 | 64 | -m , --mode 66 | image processing mode 67 | 68 | -e , --output_extention 69 | extention to output image file when output_path is (auto) or 70 | input_path is folder 71 | 72 | -l , --input_extention_list 73 | extention to input image file when input_path is folder 74 | 75 | -o , --output_path 76 | path to output image file (when input_path is folder, output_path must 77 | be folder) 78 | 79 | -i , --input_path 80 | (required) path to input image file 81 | 82 | --, --ignore_rest 83 | Ignores the rest of the labeled arguments following this flag. 84 | 85 | --version 86 | Displays version information and exits. 87 | 88 | -?, --help 89 | Displays usage information and exits. 90 | 91 | 92 | waifu2x reimplementation using Caffe 93 | ``` 94 | -------------------------------------------------------------------------------- /image-comparisons/sample1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwsou/video2x/7bbc732292b8ce731abbfcd9c80271ad61ac92f5/image-comparisons/sample1.png -------------------------------------------------------------------------------- /image-comparisons/sample2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwsou/video2x/7bbc732292b8ce731abbfcd9c80271ad61ac92f5/image-comparisons/sample2.png -------------------------------------------------------------------------------- /image-comparisons/sample3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwsou/video2x/7bbc732292b8ce731abbfcd9c80271ad61ac92f5/image-comparisons/sample3.png -------------------------------------------------------------------------------- /image-comparisons/sample4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwsou/video2x/7bbc732292b8ce731abbfcd9c80271ad61ac92f5/image-comparisons/sample4.png -------------------------------------------------------------------------------- /image-comparisons/sample5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwsou/video2x/7bbc732292b8ce731abbfcd9c80271ad61ac92f5/image-comparisons/sample5.png -------------------------------------------------------------------------------- /image-comparisons/sample6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwsou/video2x/7bbc732292b8ce731abbfcd9c80271ad61ac92f5/image-comparisons/sample6.png -------------------------------------------------------------------------------- /lib/console-utils.js: -------------------------------------------------------------------------------- 1 | const printf = require('printf'), 2 | readline = require('readline'); 3 | 4 | var timerObjects = []; 5 | var startLoadAnimation = function() { 6 | var P = [ '\\', '|', '/', '-' ]; 7 | var i = 0; 8 | var timerObj = setInterval(function() { 9 | process.stdout.write('\r' + P[i++]); 10 | i = i % P.length; 11 | }, 250); 12 | timerObjects.push(timerObj); 13 | return timerObj; 14 | }; 15 | 16 | var stopLoadAnimation = function(timerObject) { 17 | clearInterval(timerObject); 18 | timerObjects.pop(); 19 | readline.cursorTo(process.stdout, 0); 20 | }; 21 | 22 | var showError = function(err) { 23 | while(timerObjects.length > 0) { 24 | stopLoadAnimation(); 25 | } 26 | console.error(err); 27 | process.exit(-1); 28 | }; 29 | 30 | var currentIndent = 0; 31 | var enterStep = function(purposeString) { 32 | _printWithIndent(currentIndent, purposeString); 33 | currentIndent++; 34 | }; 35 | 36 | var exitStep = function() { 37 | currentIndent--; 38 | }; 39 | 40 | var printCLI = function(commandLine) { 41 | _printWithIndent(currentIndent, printf('Command: %s', commandLine)); 42 | }; 43 | 44 | var stepComment = function(purposeString) { 45 | _printWithIndent(currentIndent, purposeString); 46 | }; 47 | 48 | var _printWithIndent = function(tab, string) { 49 | // clear the entire line to remove residue output from loading animation 50 | process.stdout.write('\r'); 51 | // a tab is 4 spaces, fight me 52 | console.log(printf('%*s%s', '', tab * 4, string)); 53 | }; 54 | 55 | exports.startLoadAnimation = startLoadAnimation; 56 | exports.stopLoadAnimation = stopLoadAnimation; 57 | exports.showError = showError; 58 | exports.enterStep = enterStep; 59 | exports.exitStep = exitStep; 60 | exports.printCLI = printCLI; 61 | exports.stepComment = stepComment; -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const execFile = require('child_process').execFile, 2 | ffmpegCommand = require('fluent-ffmpeg'), 3 | fs = require('fs-extra'), 4 | path = require('path'), 5 | printf = require('printf'), 6 | Q = require('q'); 7 | 8 | const cutils = require('./console-utils'); 9 | 10 | var performStep = function(stepPurpose, callback, callbackArgsArray) { 11 | var d = Q.defer(); 12 | 13 | cutils.enterStep(stepPurpose); 14 | var loadTimer = cutils.startLoadAnimation(); 15 | callback.apply(null, callbackArgsArray).then(function(payload) { 16 | cutils.stopLoadAnimation(loadTimer); 17 | cutils.exitStep(); 18 | d.resolve(payload); 19 | }, function(err) { 20 | d.reject(err); 21 | }); 22 | 23 | return d.promise; 24 | }; 25 | 26 | var extractPNGFromSourceVideo = function(ffmpegSettings, sourcePathname, destPath, filePattern) { 27 | var d = Q.defer(); 28 | ffmpegCommand() 29 | .addInput(sourcePathname) 30 | .output(path.join(destPath, filePattern)) 31 | .withOption('-threads', ffmpegSettings.numThreads || 0) 32 | .on('start', function(commandLine) { 33 | cutils.printCLI(commandLine); 34 | }) 35 | .on('error', function(err, stdout, stderr) { 36 | d.reject(err); 37 | }) 38 | .on('end', function() { 39 | d.resolve(); 40 | }) 41 | .run() 42 | ; 43 | return d.promise; 44 | }; 45 | 46 | var combinePNGToNewVideo = function(metadata, ffmpegSettings, sourcePathname, framesPathname, destPathname) { 47 | var d = Q.defer(); 48 | 49 | var videoSettings = [ 50 | '-map 0:v', 51 | printf('%s %s', '-vcodec', ffmpegSettings.encoderLib) 52 | ]; 53 | ffmpegSettings.encodingOptions[ffmpegSettings.encoderLib].forEach(function(arg) { 54 | videoSettings.push(printf('%s %s', arg.flag, '' + arg.value)); 55 | }); 56 | 57 | ffmpegCommand() 58 | // first input: upscaled frames 59 | .input(framesPathname) 60 | .inputFps(metadata.stream.avg_frame_rate) 61 | .inputFormat('image2') 62 | // second input: original source (to copy over all non-video streams) 63 | .input(sourcePathname) 64 | // preserve all metadata information on the global level from second input 65 | .withOption('-map_metadata 1') 66 | .outputOptions(videoSettings) 67 | .withOption('-threads', ffmpegSettings.numThreads || 0) 68 | // preserve all audio, subtitle, and attachment streams from original source 69 | .outputOptions([ '-map 1:a', '-acodec copy', '-map 1:s?', '-map 1:t?' ]) 70 | .output(destPathname) 71 | .on('start', function(commandLine) { 72 | cutils.printCLI(commandLine); 73 | }) 74 | .on('error', function(err, stdout, stderr) { 75 | d.reject(stderr); 76 | }) 77 | .on('end', function() { 78 | d.resolve(); 79 | }) 80 | .run() 81 | ; 82 | return d.promise; 83 | }; 84 | 85 | var getVideoMetadata = function(file) { 86 | var d = Q.defer(); 87 | 88 | cutils.printCLI(printf('ffprobe -of json -show_streams -show_format "%s"', file)); 89 | ffmpegCommand.ffprobe(file, function(err, data) { 90 | if(err || !data) { 91 | d.reject(err || 'Input file is not valid'); 92 | return; 93 | } 94 | 95 | var result = { 96 | format: data.format 97 | }; 98 | 99 | // find the first video stream out of all available streams in metadata 100 | for(var i = 0; i < data.streams.length; i++) { 101 | if(data.streams[i].codec_type == 'video') { 102 | result.stream = data.streams[i]; 103 | break; 104 | } 105 | } 106 | d.resolve(result); 107 | }); 108 | 109 | return d.promise; 110 | }; 111 | 112 | var spawnWaifu2x = function(id, baseDirectory, waifu2xStaticArgs, newWidth, newHeight, originalFramePath, newFramePath, modelDirectory) { 113 | var d = Q.defer(); 114 | 115 | var waifu2xCUIPath = path.join(baseDirectory, 'waifu2x-caffe-cui.exe'); 116 | var args = []; 117 | [ 118 | { 119 | 'flag': '-i', 120 | 'value': originalFramePath 121 | }, 122 | { 123 | 'flag': '-o', 124 | 'value': newFramePath 125 | }, 126 | { 127 | 'flag': '-w', 128 | 'value': newWidth 129 | }, 130 | { 131 | 'flag': '-h', 132 | 'value': newHeight 133 | }, 134 | { 135 | 'flag': '--model_dir', 136 | 'value': path.join(baseDirectory, modelDirectory) 137 | } 138 | ].concat(waifu2xStaticArgs).map(function(arg) { 139 | args.push(printf('%s %s', arg.flag, '' + arg.value)); 140 | }); 141 | 142 | cutils.printCLI(printf('%s %s', waifu2xCUIPath, args.join(' '))); 143 | const WHITE_SPACE = ' '; 144 | if(originalFramePath.indexOf(WHITE_SPACE) >= 0 || newFramePath.indexOf(WHITE_SPACE) >= 0) { 145 | d.reject('waifu2xCUI does not like spaces in the input/output path'); 146 | return d.promise; 147 | } 148 | 149 | execFile(waifu2xCUIPath, args, function(err, stdout, stderr) { 150 | if(err || stderr) { 151 | d.reject(err || stderr); 152 | return; 153 | } 154 | d.resolve(id); 155 | }); 156 | 157 | return d.promise; 158 | }; 159 | 160 | // calculate total size of frames folder (1 level deep) 161 | var getDirectorySize = function(dirPath) { 162 | var d = Q.defer(); 163 | 164 | var results = { 165 | totalSize: 0, 166 | files: [] 167 | }; 168 | 169 | fs.readdir(dirPath).then(function(files) { 170 | var statsDeferred = []; 171 | 172 | results.files = files; 173 | for(var i = 0; i < files.length; i++) { 174 | statsDeferred.push(fs.stat(path.join(dirPath, files[i]))); 175 | } 176 | 177 | Q.allSettled(statsDeferred).then(function(stats) { 178 | stats.forEach(function(stat) { 179 | if(!stat.value.isDirectory()) { 180 | results.totalSize += stat.value.size; 181 | } 182 | }); 183 | d.resolve(results); 184 | }, d.reject); 185 | }, d.reject) 186 | 187 | return d.promise; 188 | }; 189 | 190 | var timeDiffMins = function(startTime) { 191 | return Math.abs(Date.now() - startTime) / (1000 * 60); 192 | }; 193 | 194 | var timeDiffHrs = function(startTime) { 195 | return Math.abs(Date.now() - startTime) / (1000 * 60 * 60); 196 | }; 197 | 198 | exports.performStep = performStep; 199 | exports.extractPNGFromSourceVideo = extractPNGFromSourceVideo; 200 | exports.combinePNGToNewVideo = combinePNGToNewVideo; 201 | exports.getVideoMetadata = getVideoMetadata; 202 | exports.spawnWaifu2x = spawnWaifu2x; 203 | exports.getDirectorySize = getDirectorySize; 204 | exports.timeDiffMins = timeDiffMins; 205 | exports.timeDiffHrs = timeDiffHrs; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video2x", 3 | "version": "1.0.2", 4 | "description": "Video upscaler using popular upscaling algorithm waifu2x to magnify and denoise artifacts from low resolution videos", 5 | "main": "app.js", 6 | "bin": "./app.js", 7 | "dependencies": { 8 | "ffmpeg": "^0.0.4", 9 | "fluent-ffmpeg": "^2.1.2", 10 | "fs-extra": "^5.0.0", 11 | "minimist": "^1.2.0", 12 | "printf": "^0.5.1", 13 | "q": "^1.5.1", 14 | "uuid": "^3.3.2" 15 | }, 16 | "devDependencies": { 17 | "grunt": "^1.0.3", 18 | "grunt-contrib-clean": "^1.1.0", 19 | "grunt-contrib-compress": "^1.4.3", 20 | "grunt-contrib-copy": "^1.0.0", 21 | "grunt-exec": "^3.0.0", 22 | "grunt-move": "^0.1.5", 23 | "grunt-textfile": "^0.2.0" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/kwsou/video2x.git" 28 | }, 29 | "homepage": "https://github.com/kwsou/video2x/releases", 30 | "scripts": { 31 | "test": "node app.js -c \"./config/default.js\"" 32 | }, 33 | "pkg": { 34 | "script": [ 35 | "lib/*.js" 36 | ], 37 | "assets": [], 38 | "targets": [ 39 | "win" 40 | ] 41 | }, 42 | "author": "", 43 | "license": "ISC" 44 | } 45 | -------------------------------------------------------------------------------- /template/version.tpl: -------------------------------------------------------------------------------- 1 | <%= name %> (v<%= version %>) 2 | <%= description %> 3 | 4 | To obtain the latest version: 5 | <%= link %> 6 | 7 | Built on <%= grunt.template.date("yyyy-mm-dd") %> at <%= grunt.template.date("hh:MM:ss TT Z") %>. 8 | -------------------------------------------------------------------------------- /test-vids/10-second-sample-Prince-of-Tennis.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwsou/video2x/7bbc732292b8ce731abbfcd9c80271ad61ac92f5/test-vids/10-second-sample-Prince-of-Tennis.mkv -------------------------------------------------------------------------------- /test-vids/20-second-sample-Aishiteru-ze-Baby.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwsou/video2x/7bbc732292b8ce731abbfcd9c80271ad61ac92f5/test-vids/20-second-sample-Aishiteru-ze-Baby.mkv -------------------------------------------------------------------------------- /test-vids/30-second-sample-Inuyasha.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwsou/video2x/7bbc732292b8ce731abbfcd9c80271ad61ac92f5/test-vids/30-second-sample-Inuyasha.mkv --------------------------------------------------------------------------------