Added tests and more

This commit is contained in:
Hannes committed 2026-03-22 03:21:39 +01:00
1 parent 295177be6f
commit a7a7abf720
9 files changed
+1327

No files matched your search

+269
View File
@@ -0,0 +1,269 @@
from pathlib import Path
import subprocess
import numpy as np
from scipy.signal import correlate
import tempfile
import soundfile as sf
import matplotlib.pyplot as plt
import subprocess
import sys
import os
import glob
import shutil
DEBUG = False
def run(cmd):
"""
Run a subprocess command.
Args:
cmd: Command and arguments as a list.
Raises:
subprocess.CalledProcessError: If the command exits with a non-zero status.
"""
try:
result = subprocess.run(
cmd,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
return result
except subprocess.CalledProcessError as e:
print(e.stdout)
raise
def extract_audio(file: Path, duration: float):
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
subprocess.run([
"ffmpeg",
"-y",
"-i", str(file),
"-map", "0:a:0", # first audio stream
"-ac", "1", # mono
"-ar", "44100", # fixed sample rate
"-t", str(duration), # limit duration
tmp.name
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
data, sr = sf.read(tmp.name)
return data, sr
def get_audio_offset(file1: Path, file2: Path, important_time: float):
sig1, sr1 = extract_audio(file1, important_time)
sig2, sr2 = extract_audio(file2, important_time)
if sr1 != sr2:
raise ValueError("Sample rates do not match")
# normalize signals
sig1 = sig1 - np.mean(sig1)
sig2 = sig2 - np.mean(sig2)
corr = correlate(sig1, sig2, mode="full")
lag = np.argmax(corr) - (len(sig2) - 1)
offset_seconds = lag / sr1
return offset_seconds
def probe_video(path: Path):
"""Get video FPS, duration, codec, and stream start time."""
import json
cmd = [
"ffprobe",
"-v", "error",
"-print_format", "json",
"-show_streams",
"-show_format",
path
]
data = json.loads(subprocess.run(cmd, capture_output=True, text=True, check=True).stdout)
duration = float(data["format"]["duration"])
for s in data["streams"]:
if s["codec_type"] == "video":
num, den = s["r_frame_rate"].split("/")
fps = float(num) / float(den)
codec = s["codec_name"]
start_time = float(s.get("start_time", 0))
return fps, duration, codec, start_time
raise RuntimeError("No video stream found")
def trim_audio(German_Videout_mkv: Path, start_time: float, end_time: float, out_audio: Path):
"""
Extract a segment of the first audio track from a video file.
Uses FFmpeg to copy the selected audio range without re-encoding,
which makes the operation fast and preserves the original audio quality.
Args:
German_Videout_mkv: Path to the source video file.
start_time: Start time of the audio segment (seconds or FFmpeg time format).
end_time: End time of the audio segment.
out_audio: Path to the output audio file.
Notes:
The function selects the first audio stream (`0:a:0`) and copies it
directly (`-c:a copy`), meaning the codec and quality remain unchanged.
"""
run([
"ffmpeg",
"-y",
"-i", German_Videout_mkv,
"-ss", str(start_time),
"-to", str(end_time),
"-map", "0:a:0", # first audio track
"-c:a", "copy", # no re-encode
out_audio
])
def mux_audio_into_output(Quality_Video, audio_file, output_dir):
"""Mux trimmed audio into Quality_Video and place final file in output_dir."""
output_dir = Path(output_dir)
output_dir.mkdir(exist_ok=True)
# final_output = output_dir / f"{Path(Quality_Video).stem}_ger.mkv"
final_output = output_dir / f"{Path(Quality_Video).stem}.mkv"
run([
"mkvmerge",
"-o", str(final_output),
str(Quality_Video),
"--language", "0:deu", # assign German language
"--track-name", "0:German (2.0 aac)",
str(audio_file)
])
return final_output
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.
"""
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 process_one(German_Video: Path, Quality_Video: Path, output_dir: Path):
# --- Probe video ---
fps, duration, _, vstart = probe_video(German_Video)
fps2, duration2, _, vstart2 = probe_video(Quality_Video)
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")
offset = get_audio_offset(Quality_Video, German_Video, 30)
print(f"Offset: {offset}s")
if offset > duration or offset > duration2:
raise ValueError("Offset needs to be smaller than the video durations")
# print(f"fps: {fps}")
# print(f"Cutting audio from frame {n1} (timestamp {start:.6f}s) to timestamp {end:.6f}s")
# --- Trim only audio ---
german_audio_file = output_dir / f"{Path(Quality_Video).stem}_german_audio.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"
if os.path.isfile(quality_audio_file1):
os.remove(quality_audio_file1)
if os.path.isfile(quality_audio_file2):
os.remove(quality_audio_file2)
start2 = vstart2
end2 = start2 + offset
if end2 > start2:
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)
start = vstart
end = start + duration
if DEBUG: print(f"Cutting audio ({german_audio_file}) (from timestamp {start:.6f}s) to timestamp {end:.6f}s")
trim_audio(German_Video, start, end, german_audio_file)
# Get the end from the first original audio
# start2 = vstart2 + end
# end2 = start2 + (duration2 - duration - offset)
# if end2 > start2:
# 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)
if os.path.isfile(quality_audio_file1):
stick_together_audio(quality_audio_file1, german_audio_file, german_audio_file)
if os.path.isfile(quality_audio_file2):
stick_together_audio(german_audio_file, quality_audio_file2, german_audio_file)
# --- Mux trimmed audio ---
final_file = mux_audio_into_output(Quality_Video, german_audio_file, output_dir)
print(f"Final file with trimmed audio created at: {final_file}")
print(f"Removing tmp Audiofiles")
if os.path.isfile(german_audio_file):
os.remove(german_audio_file)
if os.path.isfile(quality_audio_file1):
os.remove(quality_audio_file1)
if os.path.isfile(quality_audio_file2):
os.remove(quality_audio_file2)
def main():
output_dir = "/home/honney/projects/mkv/output_dir"
# for i in range(1, 14):
for i in [7, 8, 10, 11, 13, 16, 17, 19, 20, 22, 25]:
# --- German_Videouts ---
pattern = f"AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv"
German_Video = glob.glob(pattern)
pattern = f"Anohana - The Flower We Saw That Day/S01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv"
Quality_Video = glob.glob(pattern)
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()
if __name__ == "__main__":
if len(sys.argv) == 3:
print(sys.argv)
print(f"German_Video={sys.argv[1]}")
print(f"Quality_Video={sys.argv[2]}")
process_one(German_Video=Path(sys.argv[1]), Quality_Video=Path(sys.argv[2]), output_dir=Path("./output_dir"))
elif len(sys.argv) == 4:
print(sys.argv)
print(f"German_Video={sys.argv[1]}")
print(f"Quality_Video={sys.argv[2]}")
process_one(German_Video=Path(sys.argv[1]), Quality_Video=Path(sys.argv[2]), output_dir=Path(sys.argv[3]))
else:
# print(len(sys.argv), sys.argv)
# sys.exit()
main()