Some fixes
This commit is contained in:
1 parent
69e9a9037c
commit
afe90c817b
2 files changed
+89
-124
No files matched your search
+57
-124
@@ -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")
|
||||
|
||||
if offset == None:
|
||||
n1, offset = find_offset(Quality_Video, German_Video)
|
||||
print(f"Offset: {n1}")
|
||||
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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user