A lot of bug fixes and edge cases

This commit is contained in:
Hannes committed 2026-03-22 03:20:45 +01:00
1 parent 3e81425f84
commit 525b9d4dc6
5 files changed
+177 -46

No files matched your search

+1
View File
@@ -8,6 +8,7 @@ from .audio_utils import (
get_audio_streams, get_audio_streams,
get_track_info, get_track_info,
shift_audio, shift_audio,
add_silence,
replace_with_shifted_mka replace_with_shifted_mka
) )
+109 -26
View File
@@ -35,8 +35,9 @@ def extract_audio_segment_file(video: Path, start_time: float, end_time: float,
"-i", video, "-i", video,
"-ss", str(start_time), "-ss", str(start_time),
"-to", str(end_time), "-to", str(end_time),
"-map", "0:a:0", # first audio track "-map", "0:a:0",
"-c:a", "copy", # no re-encode "-ac", "2", # force stereo
"-c:a", "aac", # re-encode
out_audio out_audio
]) ])
@@ -152,32 +153,55 @@ def get_audio_duration(file_path: Path):
res += float(result[i])*(60**i) res += float(result[i])*(60**i)
return res return res
def stick_together_audio(left_audio: Path, right_audio: Path, output_audio: Path) -> None: ### V1
""" # def stick_together_audio(left_audio: Path, right_audio: Path, output_audio: Path) -> None:
Concatenate two audio files so that left_audio plays directly # """
before right_audio in the output file. # Concatenate two audio files so that left_audio plays directly
""" # before right_audio in the output file.
# """
# left_audio = Path(left_audio)
# right_audio = Path(right_audio)
# output_audio = Path(output_audio)
# # Use stem instead of prefix to create a temporary file
# output_audio_tmp = output_audio.parent / (output_audio.stem + "_temp" + output_audio.suffix)
# run([
# "ffmpeg",
# "-y",
# "-i", str(left_audio),
# "-i", str(right_audio),
# "-filter_complex", "[0:a][1:a]concat=n=2:v=0:a=1[a]",
# "-map", "[a]",
# "-c:a", "opus", # or "aac" if you prefer
# "-strict", "-2",
# str(output_audio_tmp)
# ])
# shutil.move(output_audio_tmp, output_audio)
def stick_together_audio(left_audio: Path, right_audio: Path, output_audio: Path) -> None:
left_audio = Path(left_audio) left_audio = Path(left_audio)
right_audio = Path(right_audio) right_audio = Path(right_audio)
output_audio = Path(output_audio) output_audio = Path(output_audio)
# Use stem instead of prefix to create a temporary file with tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt") as f:
output_audio_tmp = output_audio.parent / (output_audio.stem + "_temp" + output_audio.suffix) f.write(f"file '{left_audio.resolve()}'\n")
f.write(f"file '{right_audio.resolve()}'\n")
list_file = f.name
try:
run([ run([
"ffmpeg", "ffmpeg",
"-y", "-y",
"-i", str(left_audio), "-f", "concat",
"-i", str(right_audio), "-safe", "0",
"-filter_complex", "[0:a][1:a]concat=n=2:v=0:a=1[a]", "-i", list_file,
"-map", "[a]", "-c", "copy", # no re-encode
"-c:a", "opus", # or "aac" if you prefer str(output_audio)
"-strict", "-2",
str(output_audio_tmp)
]) ])
except Exception as e:
shutil.move(output_audio_tmp, output_audio) raise BrokenPipeError(f"{e}\n{f.readlines(-1)}")
def get_audio_streams(file: Path) -> List[int]: def get_audio_streams(file: Path) -> List[int]:
""" """
@@ -224,6 +248,9 @@ def get_track_info(file: Path, audioline: int):
return {"codec": "aac", "bitrate": "192k", "lang": "eng", "title": "Audio"} return {"codec": "aac", "bitrate": "192k", "lang": "eng", "title": "Audio"}
def shift_audio(file: Path, audioline: int, offset: float): def shift_audio(file: Path, audioline: int, offset: float):
"""
Shifts audio by offset sekonds to the right.
"""
if not file.is_file(): return if not file.is_file(): return
info = get_track_info(file, audioline) info = get_track_info(file, audioline)
@@ -258,7 +285,7 @@ def shift_audio(file: Path, audioline: int, offset: float):
"-b:a", str(info['bitrate']), "-b:a", str(info['bitrate']),
str(temp_mka) str(temp_mka)
] ]
subprocess.run(process_cmd, check=True) run(process_cmd)
# --- STEP 2: Replace in MKV via mkvmerge --- # --- STEP 2: Replace in MKV via mkvmerge ---
# --audio-tracks !ID excludes the old track # --audio-tracks !ID excludes the old track
@@ -271,13 +298,71 @@ def shift_audio(file: Path, audioline: int, offset: float):
str(temp_mka) str(temp_mka)
] ]
subprocess.run(merge_cmd, check=True) run(merge_cmd)
# Cleanup and Replace # Cleanup and Replace
shutil.move(str(output_mkv), str(file)) shutil.move(str(output_mkv), str(file))
temp_mka.unlink() temp_mka.unlink()
print(f"Done: {file.name} updated physically.") print(f"Done: {file.name} updated physically.")
def add_silence(file: Path, offset: float) -> None:
"""
Hard-edits an MKA file by adding silence.
Positive offset: Adds silence at the beginning.
Negative offset: Adds silence at the end.
"""
if not file.is_file():
print(f"File not found: {file}")
return
if offset == 0:
print("Offset is 0, no silence to add.")
return
# Create a normal file path right next to the original file
new_file = file.parent / f"silenced_{file.name}"
# 1. Determine the filter
if offset > 0:
# Silence at the beginning: delay all channels
ms = int(offset * 1000)
audio_filter = f"adelay={ms}:all=1"
action_text = "beginning"
else:
# Silence at the end: pad with specific duration in seconds
sec = abs(offset)
audio_filter = f"apad=pad_dur={sec}"
action_text = "end"
# 2. Execute FFmpeg
cmd = [
"ffmpeg", "-y",
"-i", str(file),
"-af", audio_filter,
"-c:a", "libopus",
str(new_file)
]
# print(f"Adding {abs(offset)}s of silence to the {action_text} of {file.name}...")
result = subprocess.run(cmd, capture_output=True, text=True)
# 3. Verify and replace
if result.returncode == 0:
# Quick safety check to ensure FFmpeg didn't output an empty file
if new_file.exists() and new_file.stat().st_size > 0:
shutil.move(str(new_file), str(file))
# print(f"Successfully updated: {file.name}")
else:
print("Error: FFmpeg succeeded, but the output file is empty.")
if new_file.exists():
new_file.unlink()
else:
print(f"FFmpeg Error:\n{result.stderr}")
# Clean up the leftover file if the process failed
if new_file.exists():
new_file.unlink()
def replace_with_shifted_mka(mkv_file: Path, mka_source: Path, audioline: int, offset: float): def replace_with_shifted_mka(mkv_file: Path, mka_source: Path, audioline: int, offset: float):
""" """
Physically shifts an existing MKA and replaces a track in the MKV. Physically shifts an existing MKA and replaces a track in the MKV.
@@ -291,10 +376,8 @@ def replace_with_shifted_mka(mkv_file: Path, mka_source: Path, audioline: int, o
# mkvmerge IDs: 0 is usually video, so audioline 0 is track 1, etc. # mkvmerge IDs: 0 is usually video, so audioline 0 is track 1, etc.
mkv_track_id = audioline + 1 mkv_track_id = audioline + 1
with tempfile.TemporaryDirectory() as temp_dir: shifted_mka = mkv_file.parent / "shifted.mka"
temp_path = Path(temp_dir) output_mkv = mkv_file.parent / "final_output.mkv"
shifted_mka = temp_path / "shifted.mka"
output_mkv = temp_path / "final_output.mkv"
# --- STEP 1: Physical Shift --- # --- STEP 1: Physical Shift ---
# Positive offset: adelay (adds real silence packets) # Positive offset: adelay (adds real silence packets)
@@ -330,4 +413,4 @@ def replace_with_shifted_mka(mkv_file: Path, mka_source: Path, audioline: int, o
# --- STEP 3: Cleanup and Overwrite --- # --- STEP 3: Cleanup and Overwrite ---
shutil.move(str(output_mkv), str(mkv_file)) shutil.move(str(output_mkv), str(mkv_file))
print(f"Success: Updated {mkv_file.name}") # print(f"Success: Updated {mkv_file.name}")
+42 -6
View File
@@ -6,13 +6,15 @@ import glob
import numpy as np import numpy as np
import subprocess import subprocess
from utils import run from utils import run
from audio import extract_audio_segment_file, get_audio_duration, stick_together_audio, replace_with_shifted_mka from audio import extract_audio_segment_file, get_audio_duration, stick_together_audio, replace_with_shifted_mka, add_silence
from video import probe_video, find_temporal_offset, corelate_videos from video import probe_video, find_temporal_offset, corelate_videos
from mkv import check_intro_outro_sync from mkv import check_intro_outro_sync
from tqdm import tqdm from tqdm import tqdm
DEBUG = False DEBUG = False
MIN_END_DUR = 2.5
def mux_audio_into_output(Quality_Video, audio_file, output_dir): def mux_audio_into_output(Quality_Video, audio_file, output_dir):
"""Mux trimmed audio into Quality_Video and place final file in output_dir.""" """Mux trimmed audio into Quality_Video and place final file in output_dir."""
output_dir = Path(output_dir) output_dir = Path(output_dir)
@@ -82,6 +84,8 @@ def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path, offse
if step == "Extract pre beginning": if step == "Extract pre beginning":
german_audio_file = output_dir / f"{Path(Quality_Video).stem}_german_audio.mka" german_audio_file = output_dir / f"{Path(Quality_Video).stem}_german_audio.mka"
# german_audio_file1 = output_dir / f"{Path(Quality_Video).stem}_german_audio1.mka"
# german_audio_file2 = output_dir / f"{Path(Quality_Video).stem}_german_audio2.mka"
quality_audio_file1 = output_dir / f"{Path(Quality_Video).stem}_original_audio1.mka" quality_audio_file1 = output_dir / f"{Path(Quality_Video).stem}_original_audio1.mka"
quality_audio_file2 = output_dir / f"{Path(Quality_Video).stem}_original_audio2.mka" quality_audio_file2 = output_dir / f"{Path(Quality_Video).stem}_original_audio2.mka"
@@ -94,6 +98,7 @@ def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path, offse
### Get the first part till german starts (if german does not start before file) ### Get the first part till german starts (if german does not start before file)
start2 = vstart2 start2 = vstart2
end2 = start2 + offset0 end2 = start2 + offset0
first_part = [start2, end2]
if end2 - start2 > 0.05: if end2 - start2 > 0.05:
if DEBUG: print(f"Cutting audio ({quality_audio_file1}) (from timestamp {start2:.6f}s) to timestamp {end2:.6f}s") if DEBUG: print(f"Cutting audio ({quality_audio_file1}) (from timestamp {start2:.6f}s) to timestamp {end2:.6f}s")
try: try:
@@ -114,6 +119,7 @@ def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path, offse
else: else:
start = vstart start = vstart
end = min(vstart + duration, start + duration2) end = min(vstart + duration, start + duration2)
main_part = [start, end]
if DEBUG: print(f"Cutting audio ({german_audio_file}) (from timestamp {start:.6f}s) to timestamp {end:.6f}s") if DEBUG: print(f"Cutting audio ({german_audio_file}) (from timestamp {start:.6f}s) to timestamp {end:.6f}s")
try: try:
extract_audio_segment_file(German_Video, start, end, german_audio_file) extract_audio_segment_file(German_Video, start, end, german_audio_file)
@@ -131,11 +137,12 @@ def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path, offse
start2 = vstart2 + end start2 = vstart2 + end
# end2 = start2 + (duration2 - duration - (n1 / fps)) # end2 = start2 + (duration2 - duration - (n1 / fps))
end2 = vstart2 + get_audio_duration(Quality_Video) end2 = vstart2 + get_audio_duration(Quality_Video)
last_part = [start2, end2]
if end2 - start2 > 0.5: if end2 - start2 > 0.5:
if DEBUG: print(f"Cutting audio ({quality_audio_file2}) (from timestamp {start2:.6f}s) to timestamp {end2:.6f}s") if DEBUG: print(f"Cutting audio ({quality_audio_file2}) (from timestamp {start2:.6f}s) to timestamp {end2:.6f}s")
try: try:
extract_audio_segment_file(Quality_Video, start2, end2, quality_audio_file2) extract_audio_segment_file(Quality_Video, start2, end2, quality_audio_file2)
except subprocess.CalledProcessError as e: except Exception as e:
raise BrokenPipeError( raise BrokenPipeError(
f"{e}\n" f"{e}\n"
f"Trying to extract Audio from {Quality_Video} {start2}-{end2} " f"Trying to extract Audio from {Quality_Video} {start2}-{end2} "
@@ -147,9 +154,32 @@ def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path, offse
# Combine the audio files # Combine the audio files
if os.path.isfile(quality_audio_file1): if os.path.isfile(quality_audio_file1):
try:
dur_q1 = get_audio_duration(quality_audio_file1)
if dur_q1:
stick_together_audio(quality_audio_file1, german_audio_file, german_audio_file) stick_together_audio(quality_audio_file1, german_audio_file, german_audio_file)
else: raise
except:
try:
add_silence(german_audio_file, offset0)
except Exception as e:
raise BrokenPipeError(
f"{e}\n"
f"Trying to put together {quality_audio_file1} and {german_audio_file}"
f"into {german_audio_file}. First file start, end is {first_part} and second is {main_part}"
)
if os.path.isfile(quality_audio_file2): if os.path.isfile(quality_audio_file2):
try:
dur_q2 = get_audio_duration(quality_audio_file2)
if dur_q2 > MIN_END_DUR:
stick_together_audio(german_audio_file, quality_audio_file2, german_audio_file) stick_together_audio(german_audio_file, quality_audio_file2, german_audio_file)
except:
raise BrokenPipeError(
f"{e}\n"
f"Trying to put together {german_audio_file} and {quality_audio_file2}"
f"into {german_audio_file}. First file start, end is {main_part} and second is {last_part}"
)
if step == "Mux": if step == "Mux":
@@ -162,6 +192,7 @@ def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path, offse
os.remove(quality_audio_file1) os.remove(quality_audio_file1)
if os.path.isfile(quality_audio_file2): if os.path.isfile(quality_audio_file2):
os.remove(quality_audio_file2) os.remove(quality_audio_file2)
# pass
if step == "Final check": if step == "Final check":
try: try:
@@ -193,19 +224,24 @@ def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path, offse
def main(): def main():
output_dir = "./output_dir" output_dir = "./output_dir"
start_num = 1 if not os.path.isdir(output_dir):
episode_num = 11 raise NotADirectoryError(f"Output directory is not a valid Directory: {output_dir}")
start_num = 2
episode_num = 24
fault_catch = [] fault_catch = []
episodes = range(start_num, episode_num + 1) episodes = range(start_num, episode_num + 1)
# episodes = [1]
for i in tqdm(episodes, desc="Episodes"): for i in tqdm(episodes, desc="Episodes"):
german_pattern = f"Folder0/S01E{i:02}*.mkv" # i =1
# if i:
german_pattern = f"./dubbed_source/Season 01/Example Series S01E{i:02}.mkv"
if os.path.isfile(german_pattern): if os.path.isfile(german_pattern):
German_Video = [german_pattern] German_Video = [german_pattern]
else: else:
German_Video = glob.glob(german_pattern) German_Video = glob.glob(german_pattern)
quality_pattern = f"Folder1/S01E{i:02}*.mkv" quality_pattern = f"./high_quality/S01/E{i:02}*"
if os.path.isfile(quality_pattern): if os.path.isfile(quality_pattern):
Quality_Video = [quality_pattern] Quality_Video = [quality_pattern]
else: else:
+5 -2
View File
@@ -35,8 +35,6 @@ from .intro_outro importme, duration) for each chapter."""
def get_intro_outro(file: Path, length: float = 90.0) -> List[Tuple[float, float]]: def get_intro_outro(file: Path, length: float = 90.0) -> List[Tuple[float, float]]:
try: try:
chapters = get_chapters(Path(file)) chapters = get_chapters(Path(file))
except:
raise AttributeError(f"{file} has no chapter Marks")
multiplier = 2 multiplier = 2
min_len = length / multiplier min_len = length / multiplier
max_len = length * multiplier max_len = length * multiplier
@@ -61,6 +59,11 @@ def get_intro_outro(file: Path, length: float = 90.0) -> List[Tuple[float, float
raise ValueError(f"Found no outro or intro:\n{filtered}") raise ValueError(f"Found no outro or intro:\n{filtered}")
else: else:
raise ValueError(f"Found too many intros/outros:\n{filtered}") raise ValueError(f"Found too many intros/outros:\n{filtered}")
except:
raise AttributeError(f"{file} has no chapter Marks")
### TODO: Find more alternative ways to find intro and outro, possible: https://github.com/aniskip/aniskip-cli
### https://github.com/jonbarrow/open-anime-timestamps?utm_source=chatgpt.com
if __name__ == '__main__': if __name__ == '__main__':
file = "./output_dir/episode.mkv" file = "./output_dir/episode.mkv"
+20 -12
View File
@@ -7,28 +7,36 @@ sys.path.append(str(Path(__file__).resolve().parent.parent))
from utils import run from utils import run
### Normal Imports: ### Normal Imports:
from subprocess import run, PIPE
from pathlib import Path
import json
def probe_video(path: Path) -> tuple[float, float, str, float]: def probe_video(path: Path) -> tuple[float, float, str, float]:
"""
Get video FPS, duration, codec, and stream start time.
Args:
path: To Video File
Returns:
fps, duration, codec, start_time
"""
import json
cmd = [ cmd = [
"ffprobe", "ffprobe",
"-v", "error", "-v", "error",
"-print_format", "json", "-print_format", "json",
"-show_streams", "-show_streams",
"-show_format", "-show_format",
path str(path), # IMPORTANT
] ]
data = json.loads(run(cmd).stdout)
result = run(cmd, stdout=PIPE, stderr=PIPE, text=True)
if result.returncode != 0:
raise RuntimeError(f"ffprobe failed:\n{result.stderr}")
if not result.stdout.strip():
raise RuntimeError("ffprobe returned empty output")
try:
data = json.loads(result.stdout)
except json.JSONDecodeError as e:
print("RAW OUTPUT:\n", result.stdout) # debug
raise
duration = float(data["format"]["duration"]) duration = float(data["format"]["duration"])
for s in data["streams"]: for s in data["streams"]:
if s["codec_type"] == "video": if s["codec_type"] == "video":
num, den = s["r_frame_rate"].split("/") num, den = s["r_frame_rate"].split("/")