From afe90c817b221165421e2592d00fd00e387dffd0 Mon Sep 17 00:00:00 2001 From: Hannes Date: Tue, 17 Mar 2026 16:58:15 +0100 Subject: [PATCH] Some fixes --- cut_to_length.py | 185 +++++++++++++++-------------------------------- test.py | 32 ++++++++ 2 files changed, 91 insertions(+), 126 deletions(-) create mode 100644 test.py diff --git a/cut_to_length.py b/cut_to_length.py index d4374d0..0651e5c 100644 --- a/cut_to_length.py +++ b/cut_to_length.py @@ -10,6 +10,7 @@ import os import glob import tempfile import shutil +from typing import Tuple DEBUG = True TARGET_FPS = 30 @@ -37,109 +38,7 @@ def run(cmd): print(e.stdout) raise -# def get_video_signal(path): -# """ -# Compute a simple motion signal from a video. - -# The function reads a video frame-by-frame and measures the amount of -# pixel change between consecutive frames. Each frame is resized and -# converted to grayscale before calculating the absolute difference -# from the previous frame. The summed pixel differences form a 1-D -# motion signal representing how much visual change occurs over time. - -# Args: -# path: Path to the video file to analyze. - -# Returns: -# A NumPy array where each element represents the magnitude of -# frame-to-frame change for a given frame. Higher values indicate -# stronger motion or scene changes. - -# Raises: -# RuntimeError: If the video cannot be opened or read. - -# Notes: -# Frames are resized to 160×90 before processing to reduce noise -# and computational cost. -# """ -# cap = cv2.VideoCapture(path) -# motion_signal = [] -# prev_gray = None - -# while True: -# ret, frame = cap.read() -# if not ret: -# break - -# # 1. Shrink the frame (makes it faster and removes noise) -# small_frame = cv2.resize(frame, (160, 90)) -# gray = cv2.cvtColor(small_frame, cv2.COLOR_BGR2GRAY) - -# if prev_gray is not None: -# # 2. Calculate the difference between this frame and the last -# # This detects the "movement" or "cuts" -# diff = cv2.absdiff(gray, prev_gray) -# motion_signal.append(np.sum(diff)) -# else: -# motion_signal.append(0) - -# prev_gray = gray - -# cap.release() -# return np.array(motion_signal) - -# def find_offset(video_a: Path, video_b: Path) -> int: -# """ -# Estimate the temporal offset between two videos. - -# The function extracts a motion signal from each video using -# `get_video_signal` and then computes the cross-correlation between -# the two signals. The lag corresponding to the maximum correlation -# indicates the frame offset where the videos align best. - -# Args: -# video_a: Path to the first video file. -# video_b: Path to the second video file. - -# Returns: -# The offset in frames between the two videos. A positive value -# means `video_b` starts later than `video_a`, while a negative -# value means `video_b` starts earlier. - -# Raises: -# RuntimeError: If either input path does not point to a valid file. - -# Notes: -# Before computing the correlation, both signals are normalized -# (mean subtracted and divided by standard deviation) to make the -# comparison robust to differences in overall motion intensity. -# """ -# if not os.path.isfile(video_a): -# raise RuntimeError("video_a is not a file") -# if not os.path.isfile(video_b): -# raise RuntimeError("video_b is not a file") - -# # print("Extracting signals (this may take a moment)...") -# sig_a = get_video_signal(video_a) -# # print("sig_a finished") -# sig_b = get_video_signal(video_b) -# # print("sig_b finished") - -# # Normalize signals (subtract mean, divide by std dev) to improve correlation -# sig_a = (sig_a - np.mean(sig_a)) / (np.std(sig_a) + 1e-5) -# sig_b = (sig_b - np.mean(sig_b)) / (np.std(sig_b) + 1e-5) - -# # Perform Cross-Correlation -# correlation = signal.correlate(sig_a, sig_b, mode='full') -# lags = signal.correlation_lags(len(sig_a), len(sig_b), mode='full') -# # plt.plot(lags) - -# # The peak of the correlation is our offset in frames -# best_lag = lags[np.argmax(correlation)] - -# return best_lag - -def get_video_signal(path): +def get_video_signal(path: Path) -> Tuple[np.ndarray, float]: cap = cv2.VideoCapture(path) fps = cap.get(cv2.CAP_PROP_FPS) @@ -167,14 +66,14 @@ def get_video_signal(path): return np.array(motion_signal), fps -def resample_signal(sig, original_fps, target_fps): +def resample_signal(sig: np.ndarray, original_fps: float, target_fps: float): duration = len(sig) / original_fps target_length = int(duration * target_fps) return signal.resample(sig, target_length) -def find_offset(video_a, video_b): +def find_offset(video_a: Path, video_b: Path) -> Tuple[int, float]: sig_a, fps_a = get_video_signal(video_a) sig_b, fps_b = get_video_signal(video_b) @@ -221,7 +120,34 @@ def probe_video(path: Path): raise RuntimeError("No video stream found") -def trim_audio(German_Videout_mkv: Path, start_time: float, end_time: float, out_audio: Path): +def get_audio_duration(file_path: Path): + """ + Get the actual duration of the first audio stream by asking ffprobe + to count packets/frames if the stream metadata is missing. + Returns duration in seconds. + """ + + # As a last resort, use the format duration + result = subprocess.run([ + "ffprobe", + "-v", "error", + "-select_streams", "a:0", + "-show_entries", "stream_tags=DURATION", + "-of", "default=noprint_wrappers=1:nokey=1", + file_path + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + result = result.stdout.strip().split(":")[::-1] + # if DEBUG: print(f"audio duration: {result}") + res = 0 + for i in range(len(result)): + res += float(result[i])*(60**i) + return res + +def trim_audio(video: Path, start_time: float, end_time: float, out_audio: Path): """ Extract a segment of the first audio track from a video file. @@ -241,7 +167,7 @@ def trim_audio(German_Videout_mkv: Path, start_time: float, end_time: float, out run([ "ffmpeg", "-y", - "-i", German_Videout_mkv, + "-i", video, "-ss", str(start_time), "-to", str(end_time), "-map", "0:a:0", # first audio track @@ -262,7 +188,7 @@ def mux_audio_into_output(Quality_Video, audio_file, output_dir): "-o", str(final_output), str(Quality_Video), "--language", "0:deu", # assign German language - "--track-name", "0:German (2.0 aac)", + "--track-name", "0:German (Opus Stereo)", str(audio_file) ]) @@ -295,20 +221,23 @@ def stick_together_audio(left_audio: Path, right_audio: Path, output_audio: Path shutil.move(output_audio_tmp, output_audio) -def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path): +def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path, offset: None | int = None): + + print(f"Working on {Quality_Video}") + # --- Probe video --- fps, duration, _, vstart = probe_video(German_Video) fps2, duration2, _, vstart2 = probe_video(Quality_Video) - if fps != fps2: - raise ValueError("FPS of both Videos must be the same (May not work yet, more testing needs to be done)") + # if fps != fps2: + # raise ValueError("FPS of both Videos must be the same (May not work yet, more testing needs to be done)") if duration <= 0: raise ValueError("Duration of German_Video must be greater than 0") if duration2 <= 0: raise ValueError("Duration of Quality_Video must be greater than 0") - - n1, offset = find_offset(Quality_Video, German_Video) - print(f"Offset: {n1}") + if offset == None: + n1, offset = find_offset(Quality_Video, German_Video) + print(f"Offset: {n1} frames| {offset}s") if offset > duration or offset > duration2: raise ValueError("Offset needs to be smaller than the video durations") @@ -344,7 +273,7 @@ def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path): # Get the start from the original Audio start2 = vstart2 end2 = start2 + offset - if end2 > start2: + if end2 - start2 > 0.05: if DEBUG: print(f"Cutting audio ({quality_audio_file1}) (from timestamp {start2:.6f}s) to timestamp {end2:.6f}s") trim_audio(Quality_Video, start2, end2, quality_audio_file1) ### @@ -356,8 +285,11 @@ def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path): trim_audio(German_Video, start, end, german_audio_file) # Get the end from the first original audio start2 = vstart2 + end - end2 = start2 + (duration2 - duration - (n1 / fps)) - if end2 > start2: + # end2 = start2 + (duration2 - duration - (n1 / fps)) + end2 = vstart2 + get_audio_duration(Quality_Video) + # print(f"duration {duration}") + # print(f"duration2 {duration2}") + if end2 - start2 > 0.5: if DEBUG: print(f"Cutting audio ({quality_audio_file2}) (from timestamp {start2:.6f}s) to timestamp {end2:.6f}s") trim_audio(Quality_Video, start2, end2, quality_audio_file2) # else: @@ -393,22 +325,23 @@ def process_one(German_Video: Path, Quality_Video: Path, output_dir: Path): os.remove(quality_audio_file2) def main(): - output_dir = "/home/honney/mount/Media/tmp/serien/ready/Black Bullet/Season 01" - # for i in range(1, 14): - for i in range(1, 14): + output_dir = "/home/honney/mount/Media/tmp/serien/ready/Maid-sama (2010) [tmdb-65648]/Season 01" + start_num = 17 + episode_num = 26 + for i in range(start_num, episode_num+1): # --- German_Videouts --- - # German_Video = f"/home/honney/mount/Media/tmp/serien/ger_low_quality/Black Bullet/Black Bullet Season 01 E{i:02}*.mp4" - # Quality_Video = f"/home/honney/mount/Media/tmp/serien/high_quality/Black Bullet (Season 1)/Season 01/Black Bullet - S01E{i:02}.mkv" - pattern = f"/home/honney/mount/Media/tmp/serien/ger_low_quality/Black Bullet/Black Bullet Season 01 E{i:02}*.mp4" + pattern = f"/home/honney/mount/Media/tmp/serien/Maid Sama All/Maid Sama/S01/{i:02}.mkv" + print(f"German_Video={pattern}") German_Video = glob.glob(pattern) - pattern = f"/home/honney/mount/Media/tmp/serien/high_quality/Black Bullet (Season 1)/Season 01/Black Bullet - S01E{i:02}.mkv" + pattern = f"/home/honney/mount/Media/tmp/serien/Maid Sama All/Kaichou wa Maid-sama!/S01/{i:02}.mkv" Quality_Video = glob.glob(pattern) + print(f"German_Video={German_Video}") + print(f"Quality_Video={Quality_Video}") if len(German_Video) > 1: raise ValueError("There are to many videos matching the German wildcard") if len(Quality_Video) > 1: raise ValueError("There are to many videos matching the Quality wildcard") - process_one(Path(German_Video[0]), Path(Quality_Video[0]), Path(output_dir)) # sys.exit() @@ -426,4 +359,4 @@ if __name__ == "__main__": else: # print(len(sys.argv), sys.argv) # sys.exit() - main() + main() \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..2351581 --- /dev/null +++ b/test.py @@ -0,0 +1,32 @@ +import subprocess +from pathlib import Path + +def get_audio_duration(file_path: Path): + """ + Get the actual duration of the first audio stream by asking ffprobe + to count packets/frames if the stream metadata is missing. + Returns duration in seconds. + """ + + # As a last resort, use the format duration + result = subprocess.run([ + "ffprobe", + "-v", "error", + "-select_streams", "a:0", + "-show_entries", "stream_tags=DURATION", + "-of", "default=noprint_wrappers=1:nokey=1", + file_path + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + result = result.stdout.strip().split(":")[::-1] + res = 0 + for i in range(len(result)): + res += float(result[i])*(60**i) + return res + +audio_file = Path("/home/honney/mount/Media/tmp/serien/Maid Sama All/Kaichou wa Maid-sama!/S00/Maid-sama_S00E01 - OVA.mkv") +duration = get_audio_duration(audio_file) +print("Duration of first audio stream (approx.):", duration) \ No newline at end of file