Added all functionality to cut_to_length

This commit is contained in:
Hannes committed 2026-03-09 23:56:15 +01:00
1 parent 6c1687d6bc
commit cb56886a4b
5 files changed
+229 -143

No files matched your search

+1
View File
@@ -0,0 +1 @@
*/
+86 -66
View File
@@ -1,16 +1,66 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import subprocess import subprocess
import json
import sys
import shutil
from pathlib import Path from pathlib import Path
import cv2
import numpy as np
from scipy import signal
def run(cmd): def run(cmd):
return subprocess.run(cmd, capture_output=True, text=True, check=True) """Run a subprocess command and raise on error."""
subprocess.run(cmd, check=True)
def get_video_signal(path):
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, video_b):
# 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 probe_video(path): def probe_video(path):
"""Get video FPS, duration, codec, and stream start time."""
import json
cmd = [ cmd = [
"ffprobe", "ffprobe",
"-v", "error", "-v", "error",
@@ -19,11 +69,9 @@ def probe_video(path):
"-show_format", "-show_format",
path path
] ]
data = json.loads(subprocess.run(cmd, capture_output=True, text=True, check=True).stdout)
data = json.loads(run(cmd).stdout)
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("/")
@@ -34,95 +82,67 @@ def probe_video(path):
raise RuntimeError("No video stream found") raise RuntimeError("No video stream found")
def vaapi_cut(inp, outp, start, end, codec="h264_vaapi"): def trim_audio(input_mkv, start_time, end_time, out_audio):
encoder = codec """Trim only the first (and only) audio track from input_mkv."""
run([
cmd = [
"ffmpeg", "ffmpeg",
"-y", "-y",
"-vaapi_device", "/dev/dri/renderD128", "-i", input_mkv,
"-ss", str(start_time),
"-ss", str(start), "-to", str(end_time),
"-to", str(end), "-map", "0:a:0", # first audio track
"-c:a", "copy", # no re-encode
"-i", inp, out_audio
"-map", "0",
"-vf", "format=nv12,hwupload",
"-c:v", encoder,
"-qp", "20",
"-c:a", "copy",
"-c:s", "copy",
"-c:d", "copy",
"-c:t", "copy",
outp
]
subprocess.run(cmd, check=True)
def extract_audio(output_mkv, out_audio):
# Extract the first (and only) audio track
run([
"mkvextract",
"tracks",
output_mkv,
f"1:{out_audio}" # track IDs are usually 0 for video, 1 for audio
]) ])
def mux_audio_into_copy(inp2, audio_file, output_dir): def mux_audio_into_output(inp2, audio_file, output_dir):
"""Mux trimmed audio into inp2 and place final file in output_dir."""
output_dir = Path(output_dir) output_dir = Path(output_dir)
output_dir.mkdir(exist_ok=True) output_dir.mkdir(exist_ok=True)
copied_video = output_dir / Path(inp2).name final_output = output_dir / f"{Path(inp2).stem}_ger.mkv"
shutil.copy2(inp2, copied_video)
final_output = output_dir / "final_with_audio.mkv"
run([ run([
"mkvmerge", "mkvmerge",
"-o", str(final_output), "-o", str(final_output),
str(copied_video), str(inp2),
"--language", "0:deu", # assign German language "--language", "0:deu", # assign German language
"--track-name", "0:German (2.0 aac)",
str(audio_file) str(audio_file)
]) ])
return final_output return final_output
def main(): def main():
inp = f"/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E001.mkv" for i in range(1, 12):
inp2 = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day - S01 (BD 1080p HEVC Opus) [derpie]/[derpie] Anohana - The Flower We Saw That Day - S01E01 (BD 1080p HEVC Opus) [Dual Audio].mkv" # --- Inputs ---
inp = f"/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv"
inp2 = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day/S01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv"
outp = "./output.mkv" n1 = find_offset(inp, inp2) # first frame to keep
n1 = 24 output_dir = "./output_dir"
# --- Probe video ---
fps, _, _, vstart = probe_video(inp) fps, _, _, vstart = probe_video(inp)
_, duration, _, _ = probe_video(inp2) _, duration2, _, _ = probe_video(inp2)
# --- Compute timestamps ---
start = vstart + (n1 / fps) start = vstart + (n1 / fps)
end = duration + start end = start + duration2
if end <= start: if end <= start:
raise ValueError("n2 must be greater than n1") raise ValueError("n2 must be greater than n1")
print(f"fps: {fps}") print(f"fps: {fps}")
print(f"cutting from frame {n1} to {end*fps}") print(f"Cutting audio from frame {n1} (timestamp {start:.6f}s) to timestamp {end:.6f}s")
print(f"time range: {start:.6f}s -> {end:.6f}s")
print("Trying stream copy...") # --- Trim only audio ---
vaapi_cut(inp, outp, start, end) audio_file = Path(output_dir) / f"{Path(inp2).stem}_german_audio.aac"
trim_audio(inp, start, end, audio_file)
output_dir = "./output_dir" # --- Mux trimmed audio ---
audio_file = Path(output_dir) / "german_audio.aac" final_file = mux_audio_into_output(inp2, audio_file, output_dir)
extract_audio(outp, audio_file) print(f"Final file with trimmed audio created at: {final_file}")
final_file = mux_audio_into_copy(inp2, audio_file, output_dir)
print(f"Final file created at: {final_file}")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+72
View File
@@ -0,0 +1,72 @@
import cv2
import numpy as np
def create_comparison_collage(video_a_path, video_b_path, time_a, offset_b, radius=1, output_name="collage.jpg"):
caps = {
"A": cv2.VideoCapture(video_a_path),
"B": cv2.VideoCapture(video_b_path)
}
results = {"A": [], "B": []}
for label, cap in caps.items():
if not cap.isOpened():
print(f"Error: Could not open Video {label}")
return
fps = cap.get(cv2.CAP_PROP_FPS)
# Determine base time for this specific video
base_time = time_a if label == "A" else (time_a + offset_b)
center_frame = int(round(base_time * fps))
# Extract range: from (center - radius) to (center + radius)
for i in range(-radius, radius + 1):
target_idx = center_frame + i
cap.set(cv2.CAP_PROP_POS_FRAMES, target_idx)
success, frame = cap.read()
if success:
# Add overlay text (Timestamp and Frame Index)
current_time = target_idx / fps
text = f"Vid {label} | F: {target_idx} | {current_time:.4f}s"
# Draw black background for text for readability
cv2.putText(frame, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,0), 4, cv2.LINE_AA)
cv2.putText(frame, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255,255,255), 2, cv2.LINE_AA)
results[label].append(frame)
else:
# Placeholder if frame is missing
blank = np.zeros((720, 1280, 3), dtype=np.uint8)
cv2.putText(blank, "FRAME MISSING", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
results[label].append(blank)
cap.release()
# Ensure all frames are the same size for the collage (using Video A's dimensions)
h, w, _ = results["A"][0].shape
rows = []
for i in range(len(results["A"])):
img_a = cv2.resize(results["A"][i], (w, h))
img_b = cv2.resize(results["B"][i], (w, h))
# Combine Left (A) and Right (B)
combined_row = np.hstack((img_a, img_b))
rows.append(combined_row)
# Combine all rows Top to Bottom
final_collage = np.vstack(rows)
# Save and show
cv2.imwrite(output_name, final_collage)
print(f"Collage saved as {output_name}")
# --- SETTINGS ---
video_a = "/home/honney/projects/mkv/Anohana - The Flower We Saw That Day - S01 (BD 1080p HEVC Opus) [derpie]/[derpie] Anohana - The Flower We Saw That Day - S01E02 (BD 1080p HEVC Opus) [Dual Audio].mkv"
video_b = "/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E002.mkv"
timestamp_a = 60 * 20#1/(24000/1001) * 250 # Center point for Video A 75
offset_for_b = 1/(24000/1001) * 24
frames_around = 10 # 2 before, 2 after (5 frames total per video)
create_comparison_collage(video_a, video_b, timestamp_a, offset_for_b, frames_around)
+13 -11
View File
@@ -2,6 +2,7 @@ import cv2
import numpy as np import numpy as np
from scipy import signal from scipy import signal
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import sys
def get_video_signal(path): def get_video_signal(path):
cap = cv2.VideoCapture(path) cap = cv2.VideoCapture(path)
@@ -52,17 +53,18 @@ def find_offset(video_a, video_b):
return best_lag return best_lag
# Usage # Usage
# for i in range(1, 12): for i in range(1, 12):
# # print(f"{i:02}")
# video_a = f"/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv"
# video_b = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day - S01 (BD 1080p HEVC Opus) [derpie]/[derpie] Anohana - The Flower We Saw That Day - S01E{i:02} (BD 1080p HEVC Opus) [Dual Audio].mkv"
# offset_frames = find_offset(video_a, video_b)
# print(f"{i}: Video B is offset by {offset_frames} frames relative to Video A.")
# print(f"{i:02}") # print(f"{i:02}")
video_a = f"/home/honney/projects/mkv/output.mkv" video_a = f"/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv"
video_b = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day - S01 (BD 1080p HEVC Opus) [derpie]/[derpie] Anohana - The Flower We Saw That Day - S01E01 (BD 1080p HEVC Opus) [Dual Audio].mkv" video_b = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day/S01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv"
offset_frames = find_offset(video_a, video_b) offset_frames = find_offset(video_a, video_b)
print(f"Video B is offset by {offset_frames} frames relative to Video A.") print(f"{i}: Video B is offset by {offset_frames} frames relative to Video A.")
# sys.exit()
# video_a = f"/home/honney/projects/mkv/output_dir/AnoHana The Flower We Saw That Day S01E001_ger.mkv"
# video_b = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day/S01/AnoHana The Flower We Saw That Day S01E001.mkv"
# offset_frames = find_offset(video_a, video_b)
# print(f"Video B is offset by {offset_frames} frames relative to Video A.")
# sys.exit()
+57 -66
View File
@@ -1,104 +1,95 @@
import subprocess import subprocess
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import sys from scipy.interpolate import interp1d
# --- Setup --- # --- Setup ---
video_a = "/home/honney/projects/mkv/Saga of Tanya the Evil/Season 1/Saga Of Tanya The Evil - S01E01 - The Devil Of The Rhine.mkv" video_a = "/home/honney/projects/mkv/Anohana - The Flower We Saw That Day - S01 (BD 1080p HEVC Opus) [derpie]/[derpie] Anohana - The Flower We Saw That Day - S01E01 (BD 1080p HEVC Opus) [Dual Audio].mkv"
video_b = "/home/honney/projects/mkv/Saga of Tanya the Evil (2017-2017) [imdbid-tt6455986]/Season 01/Saga of Tanya the Evil S01E001.mkv" video_b = "/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E001.mkv"
FPS_NATIVE = 24000/1001
WIDTH = 160 WIDTH = 160
HEIGHT = int((WIDTH/16)*9) HEIGHT = int((WIDTH/16)*9)
FPS = 24000/1001 MAX_OFFSET_SECONDS = 1
MAX_OFFSET_SECONDS = 2 # ±2 Sekunden Suche
# FAKTOR FÜR PRÄZISION:
# 2 = Doppelte FPS für die Rechnung (Sub-Frame)
# 10 = Zehnfache Präzision (reicht für Mikrosekunden-Sync)
UPSAMPLE_FACTOR = 10
FPS_VIRTUAL = FPS_NATIVE * UPSAMPLE_FACTOR
def extract_hist_signal(video): def extract_hist_signal(video):
"""Extrahiert pro Frame einen Histogrammvektor unter Nutzung von VA-API (AMD GPU)."""
# Wir nutzen vaapi für Hardware-Dekodierung und Scaling
proc = subprocess.Popen([ proc = subprocess.Popen([
"ffmpeg", "ffmpeg", "-loglevel", "error", "-i", video,
"-loglevel", "error", "-vf", f"scale={WIDTH}:{HEIGHT},format=gray",
"-hwaccel", "vaapi", # Nutze VA-API Hardware-Beschleunigung "-f", "rawvideo", "-"
"-hwaccel_device", "/dev/dri/renderD128", # Pfad zur RX 7900 XTX
"-hwaccel_output_format", "vaapi",
"-i", video,
"-vf", (
f"fps={FPS}," # Framerate Anpassung
f"scale_vaapi=w={WIDTH}:h={HEIGHT}:format=gray8," # GPU-Scaling + Grayscale
"hwdownload,format=gray8" # Zurück in den RAM schieben
),
"-f", "rawvideo",
"-"
], stdout=subprocess.PIPE) ], stdout=subprocess.PIPE)
frame_size = WIDTH * HEIGHT frame_size = WIDTH * HEIGHT
hist_signal = [] hist_signal = []
# Der restliche Loop bleibt gleich, da die Histogramm-Berechnung
# (np.histogram) weiterhin auf der CPU via NumPy erfolgt.
while True: while True:
data = proc.stdout.read(frame_size) data = proc.stdout.read(frame_size)
if not data or len(data) < frame_size: if not data or len(data) < frame_size: break
break
frame = np.frombuffer(data, dtype=np.uint8) frame = np.frombuffer(data, dtype=np.uint8)
hist, _ = np.histogram(frame, bins=32, range=(0, 255)) hist, _ = np.histogram(frame, bins=64, range=(0, 255))
hist_signal.append(hist / np.sum(hist)) hist_signal.append(hist / (np.sum(hist) + 1e-6))
proc.wait() proc.wait()
return np.array(hist_signal) return np.array(hist_signal)
print(f"Extracting signals (Native FPS: {FPS_NATIVE:.4f})...")
a_raw = extract_hist_signal(video_a)
b_raw = extract_hist_signal(video_b)
print("Extracting signal of video A") # 1. In 1D-Signal umwandeln (Helligkeits-Schnitt)
a = extract_hist_signal(video_a) sig_a_raw = np.mean(a_raw, axis=1)
sig_b_raw = np.mean(b_raw, axis=1)
print("Extracting signal of video B") # 2. UPSAMPLING (Die "doppelte FPS" Logik)
b = extract_hist_signal(video_b) # Wir erstellen eine glatte Kurve durch die Frames und tasten sie feiner ab
def upsample_signal(sig, factor):
x = np.arange(len(sig))
f = interp1d(x, sig, kind='cubic') # Kubische Interpolation für weiche Übergänge
x_new = np.linspace(0, len(sig) - 1, len(sig) * factor)
return f(x_new)
print("Calculating cross correlation") print(f"Upsampling signals by factor {UPSAMPLE_FACTOR}...")
sig_a = upsample_signal(sig_a_raw, UPSAMPLE_FACTOR)
sig_b = upsample_signal(sig_b_raw, UPSAMPLE_FACTOR)
max_lag_frames = int(MAX_OFFSET_SECONDS * FPS) # 3. Zentrieren für bessere Korrelation
n_a, n_bins = a.shape sig_a -= np.mean(sig_a)
n_b, _ = b.shape sig_b -= np.mean(sig_b)
lags = np.arange(-max_lag_frames, max_lag_frames + 1) print("Calculating high-precision cross correlation...")
corr = [] corr = np.correlate(sig_a, sig_b, mode='full')
lags = np.arange(-len(sig_b) + 1, len(sig_a))
for lag in lags: # Suchbereich einschränken (jetzt basierend auf virtueller FPS)
if lag < 0: max_lag_v = int(MAX_OFFSET_SECONDS * FPS_VIRTUAL)
# Offset negativ → Video B vor Video A search_range = (lags >= -max_lag_v) & (lags <= max_lag_v)
sub_a = a[:lag] # bis lag Frames von Video A
sub_b = b[-lag:] # von -lag bis Ende von Video B
else:
sub_a = a[lag:] # ab lag bis Ende von Video A
sub_b = b[:n_b - lag] # bis n_b - lag von Video B
# Frames angleichen filtered_corr = corr[search_range]
min_len = min(len(sub_a), len(sub_b)) filtered_lags = lags[search_range]
if min_len == 0:
corr.append(0)
continue
sub_a = sub_a[:min_len]
sub_b = sub_b[:min_len]
corr.append(np.sum(sub_a * sub_b)) best_index = np.argmax(filtered_corr)
best_lag_v = filtered_lags[best_index]
corr = np.array(corr) # Der Offset berechnet sich nun aus dem virtuellen Lag und der virtuellen FPS
best_index = np.argmax(corr) best_offset_seconds = best_lag_v / FPS_VIRTUAL
best_lag = lags[best_index]
print("Best offset frames:", best_lag) print("-" * 30)
print("Best offset seconds:", best_lag / FPS) print(f"Best virtual lag: {best_lag_v} units")
print(f"Resulting frames: {best_lag_v / UPSAMPLE_FACTOR:.3f}")
sys.exit() print(f"Best offset seconds: {best_offset_seconds:.12f}")
print("-" * 30)
# Plot # Plot
plt.figure(figsize=(10, 5)) plt.figure(figsize=(12, 5))
plt.plot(lags / FPS, corr, marker='o') plt.plot(filtered_lags / FPS_VIRTUAL, filtered_corr)
plt.axvline(best_lag / FPS, color='r', linestyle='--', label=f"Best offset = {best_lag / FPS:.3f}s") plt.axvline(best_offset_seconds, color='r', linestyle='--', label=f"Peak: {best_offset_seconds:.6f}s")
plt.title(f"High-Precision Sync (Upsampled x{UPSAMPLE_FACTOR})")
plt.xlabel("Offset (seconds)") plt.xlabel("Offset (seconds)")
plt.ylabel("Cross correlation") plt.ylabel("Correlation")
plt.title("Video cross correlation (Histogram-based)")
plt.legend() plt.legend()
plt.grid(True) plt.grid(True)
plt.show() plt.show()