|
|
|
|
@@ -19,6 +19,21 @@ DIR_CONV_LOGS = Path("conv_logs") # Directory for conversion logs
|
|
|
|
|
|
|
|
|
|
REMUX_CODECS = {"aac", "opus"} # Using a set for efficient lookups
|
|
|
|
|
|
|
|
|
|
SVT_AV1_PARAMS = {
|
|
|
|
|
"speed": "slower", # "slower", "slow", "medium", "fast", "faster"
|
|
|
|
|
"quality": "medium", # "higher", "high", "medium", "low", "lower"
|
|
|
|
|
"film-grain": 6,
|
|
|
|
|
"color-primaries": 1,
|
|
|
|
|
"transfer-characteristics": 1,
|
|
|
|
|
"matrix-coefficients": 1,
|
|
|
|
|
"scd": 0, # Scene change detection OFF for Av1an use
|
|
|
|
|
"keyint": 0, # Keyframe interval, 0 disables automatic keyframes placement at a constant interval
|
|
|
|
|
"lp": 2, # Level of parallelism
|
|
|
|
|
"auto-tiling": 1, # Auto tiling ON
|
|
|
|
|
"tune": 1, # 0 = VQ, 1 = PSNR, 2 = SSIM
|
|
|
|
|
"progress": 2, # Detailed progress output
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def check_tools():
|
|
|
|
|
for tool in REQUIRED_TOOLS:
|
|
|
|
|
if shutil.which(tool) is None:
|
|
|
|
|
@@ -40,7 +55,7 @@ def convert_audio_track(index, ch, lang, audio_temp_dir, source_file, should_dow
|
|
|
|
|
|
|
|
|
|
print(f" - Extracting Audio Track #{index} to FLAC...")
|
|
|
|
|
ffmpeg_args = [
|
|
|
|
|
"ffmpeg", "-v", "quiet", "-stats", "-y", "-i", str(source_file), "-map", f"0:{index}"
|
|
|
|
|
"ffmpeg", "-v", "quiet", "-stats", "-y", "-i", str(source_file), "-map", f"0:{index}", "-map_metadata", "-1"
|
|
|
|
|
]
|
|
|
|
|
if should_downmix and ch >= 6:
|
|
|
|
|
if ch == 6:
|
|
|
|
|
@@ -68,14 +83,16 @@ def convert_audio_track(index, ch, lang, audio_temp_dir, source_file, should_dow
|
|
|
|
|
else:
|
|
|
|
|
# Not downmixing (or source is already stereo or less).
|
|
|
|
|
# Base bitrate on the source channel count.
|
|
|
|
|
if ch == 2: # Stereo
|
|
|
|
|
if ch == 1: # Mono
|
|
|
|
|
bitrate = "64k"
|
|
|
|
|
elif ch == 2: # Stereo
|
|
|
|
|
bitrate = "128k"
|
|
|
|
|
elif ch == 6: # 5.1 Surround
|
|
|
|
|
bitrate = "256k"
|
|
|
|
|
elif ch == 8: # 7.1 Surround
|
|
|
|
|
bitrate = "384k"
|
|
|
|
|
else: # Mono or other layouts
|
|
|
|
|
bitrate = "96k" # A sensible default for mono.
|
|
|
|
|
else: # Other layouts
|
|
|
|
|
bitrate = "96k" # A sensible default for other/uncommon layouts.
|
|
|
|
|
|
|
|
|
|
print(f" - Encoding Audio Track #{index} to Opus at {bitrate}...")
|
|
|
|
|
run_cmd([
|
|
|
|
|
@@ -86,7 +103,6 @@ def convert_audio_track(index, ch, lang, audio_temp_dir, source_file, should_dow
|
|
|
|
|
def convert_video(source_file_base, source_file_full, is_vfr, target_cfr_fps_for_handbrake, autocrop_filter=None):
|
|
|
|
|
print(" --- Starting Video Processing ---")
|
|
|
|
|
# source_file_base is file_path.stem (e.g., "my.anime.episode.01")
|
|
|
|
|
scene_file = Path(f"{source_file_base}.txt")
|
|
|
|
|
vpy_file = Path(f"{source_file_base}.vpy")
|
|
|
|
|
ut_video_file = Path(f"{source_file_base}.ut.mkv")
|
|
|
|
|
encoded_video_file = Path(f"temp-{source_file_base}.mkv")
|
|
|
|
|
@@ -159,39 +175,19 @@ clip.set_output()
|
|
|
|
|
with vpy_file.open("w", encoding="utf-8") as f:
|
|
|
|
|
f.write(vpy_script_content)
|
|
|
|
|
|
|
|
|
|
if not scene_file.exists():
|
|
|
|
|
print(" - Performing scene detection with av1an...")
|
|
|
|
|
av1an_sc_args = [
|
|
|
|
|
"av1an", "-i", str(vpy_file), "-s", str(scene_file), "--sc-only", "--verbose"
|
|
|
|
|
]
|
|
|
|
|
run_cmd(av1an_sc_args)
|
|
|
|
|
else:
|
|
|
|
|
print(" - Found existing scene file, skipping detection.")
|
|
|
|
|
|
|
|
|
|
print(" - Starting AV1 encode with av1an (this will take a long time)...")
|
|
|
|
|
total_cores = os.cpu_count() or 4 # Fallback if cpu_count is None
|
|
|
|
|
workers = max(total_cores - 2, 1) # Ensure at least 1 worker
|
|
|
|
|
print(f" - Using {workers} workers for av1an (Total Cores: {total_cores}).")
|
|
|
|
|
workers = max(1, (total_cores // 2) - 1) # Half the cores minus one, with a minimum of 1 worker.
|
|
|
|
|
print(f" - Using {workers} workers for av1an (Total Cores: {total_cores}, Logic: (Cores/2)-1).")
|
|
|
|
|
|
|
|
|
|
svt_av1_params = {
|
|
|
|
|
"preset": 2,
|
|
|
|
|
"crf": 27,
|
|
|
|
|
"film-grain": 6,
|
|
|
|
|
"lp": 1,
|
|
|
|
|
"tune": 1,
|
|
|
|
|
"keyint": -1,
|
|
|
|
|
"color-primaries": 1,
|
|
|
|
|
"transfer-characteristics": 1,
|
|
|
|
|
"matrix-coefficients": 1,
|
|
|
|
|
}
|
|
|
|
|
# Create the parameter string for av1an's -v option, which expects a single string.
|
|
|
|
|
av1an_video_params_str = " ".join([f"--{key} {value}" for key, value in svt_av1_params.items()])
|
|
|
|
|
av1an_video_params_str = " ".join([f"--{key} {value}" for key, value in SVT_AV1_PARAMS.items()])
|
|
|
|
|
print(f" - Using SVT-AV1 parameters: {av1an_video_params_str}")
|
|
|
|
|
|
|
|
|
|
av1an_enc_args = [
|
|
|
|
|
"av1an", "-i", str(vpy_file), "-o", str(encoded_video_file), "-s", str(scene_file), "-n",
|
|
|
|
|
"av1an", "-i", str(vpy_file), "-o", str(encoded_video_file), "-n",
|
|
|
|
|
"-e", "svt-av1", "--resume", "--sc-pix-format", "yuv420p", "-c", "mkvmerge",
|
|
|
|
|
"--set-thread-affinity", "1", "--pix-format", "yuv420p10le", "--force",
|
|
|
|
|
"--set-thread-affinity", "2", "--pix-format", "yuv420p10le", "--force",
|
|
|
|
|
"-w", str(workers),
|
|
|
|
|
"-v", av1an_video_params_str
|
|
|
|
|
]
|
|
|
|
|
@@ -270,17 +266,21 @@ def _snap_to_known_ar_cropdetect(w, h, x, y, video_w, video_h, tolerance=0.03):
|
|
|
|
|
new_h = round(video_w / best_match['ratio'])
|
|
|
|
|
if new_h % 8 != 0:
|
|
|
|
|
new_h = new_h + (8 - (new_h % 8))
|
|
|
|
|
new_h = min(new_h, video_h)
|
|
|
|
|
new_y = round((video_h - new_h) / 2)
|
|
|
|
|
if new_y % 2 != 0:
|
|
|
|
|
new_y -= 1
|
|
|
|
|
new_y = max(0, new_y)
|
|
|
|
|
return f"crop={video_w}:{new_h}:0:{new_y}", best_match['name']
|
|
|
|
|
if abs(h - video_h) < 16:
|
|
|
|
|
new_w = round(video_h * best_match['ratio'])
|
|
|
|
|
if new_w % 8 != 0:
|
|
|
|
|
new_w = new_w + (8 - (new_w % 8))
|
|
|
|
|
new_w = min(new_w, video_w)
|
|
|
|
|
new_x = round((video_w - new_w) / 2)
|
|
|
|
|
if new_x % 2 != 0:
|
|
|
|
|
new_x -= 1
|
|
|
|
|
new_x = max(0, new_x)
|
|
|
|
|
return f"crop={new_w}:{video_h}:{new_x}:0", best_match['name']
|
|
|
|
|
return f"crop={w}:{h}:{x}:{y}", None
|
|
|
|
|
|
|
|
|
|
@@ -433,8 +433,17 @@ def detect_autocrop_filter(input_file, significant_crop_threshold=5.0, min_crop=
|
|
|
|
|
return None
|
|
|
|
|
return _analyze_video_cropdetect(input_file, duration, width, height, max(1, os.cpu_count() // 2), significant_crop_threshold, min_crop, debug)
|
|
|
|
|
|
|
|
|
|
def main(no_downmix=False, autocrop=False):
|
|
|
|
|
def main(no_downmix=False, autocrop=False, speed=None, quality=None, grain=None):
|
|
|
|
|
check_tools()
|
|
|
|
|
|
|
|
|
|
# Override default SVT-AV1 params if provided via command line
|
|
|
|
|
if speed:
|
|
|
|
|
SVT_AV1_PARAMS["speed"] = speed
|
|
|
|
|
if quality:
|
|
|
|
|
SVT_AV1_PARAMS["quality"] = quality
|
|
|
|
|
if grain is not None:
|
|
|
|
|
SVT_AV1_PARAMS["film-grain"] = grain
|
|
|
|
|
|
|
|
|
|
current_dir = Path(".")
|
|
|
|
|
files_to_process = sorted(
|
|
|
|
|
f for f in current_dir.glob("*.mkv")
|
|
|
|
|
@@ -638,7 +647,6 @@ def main(no_downmix=False, autocrop=False):
|
|
|
|
|
|
|
|
|
|
print("Cleaning up persistent video temporary files (after successful processing)...")
|
|
|
|
|
video_temp_files_on_success = [
|
|
|
|
|
current_dir / f"{file_path.stem}.txt",
|
|
|
|
|
current_dir / f"{file_path.stem}.vpy",
|
|
|
|
|
current_dir / f"{file_path.stem}.ut.mkv",
|
|
|
|
|
current_dir / f"temp-{file_path.stem}.mkv", # This is encoded_video_file
|
|
|
|
|
@@ -720,5 +728,8 @@ if __name__ == "__main__":
|
|
|
|
|
parser = argparse.ArgumentParser(description="Batch-process MKV files with resumable video encoding, audio downmixing, per-file logging, and optional autocrop.")
|
|
|
|
|
parser.add_argument("--no-downmix", action="store_true", help="Preserve original audio channel layout.")
|
|
|
|
|
parser.add_argument("--autocrop", action="store_true", help="Automatically detect and crop black bars from video using cropdetect.")
|
|
|
|
|
parser.add_argument("--speed", type=str, help="Set the encoding speed. Possible values: slower, slow, medium, fast, faster.")
|
|
|
|
|
parser.add_argument("--quality", type=str, help="Set the encoding quality. Possible values: lowest, low, medium, high, higher.")
|
|
|
|
|
parser.add_argument("--grain", type=int, help="Set the film-grain value (number). Adjusts the film grain synthesis level.")
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
main(no_downmix=args.no_downmix, autocrop=args.autocrop)
|
|
|
|
|
main(no_downmix=args.no_downmix, autocrop=args.autocrop, speed=args.speed, quality=args.quality, grain=args.grain)
|
|
|
|
|
|