Загрузка данных
#!/usr/bin/env python3
"""
Автомонтаж через ffmpeg.
Шаг 1 (вырезать паузы):
python auto_edit.py cut input.mp4 clean.mp4
Шаг 2 (музыка, текст, картинки по таймкодам из edits.json):
python auto_edit.py compose clean.mp4 edits.json final.mp4
Нужны: Python 3 и ffmpeg (версия 4.4 или новее), оба в PATH.
Таймкоды в edits.json считаются по видео clean.mp4 (уже без пауз).
"""
import json
import re
import subprocess
import sys
def run(cmd):
print(" ".join(cmd))
subprocess.run(cmd, check=True)
def duration(path):
out = subprocess.check_output([
"ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=nw=1:nk=1", path])
return float(out.strip())
def cut_silence(src, dst, noise_db=-35, min_silence=0.6, pad=0.15):
"""Убирает тишину. Если режет речь - поднимите noise_db (например -30)
или увеличьте min_silence."""
p = subprocess.run(
["ffmpeg", "-i", src, "-af",
f"silencedetect=noise={noise_db}dB:d={min_silence}",
"-f", "null", "-"],
capture_output=True, text=True)
starts = [float(x) for x in re.findall(r"silence_start: (-?[\d.]+)", p.stderr)]
ends = [float(x) for x in re.findall(r"silence_end: ([\d.]+)", p.stderr)]
total = duration(src)
keep, cur = [], 0.0
for s, e in zip(starts, ends):
s = max(s, 0.0)
if s - cur > 0.2:
keep.append((cur, s + pad))
cur = max(e - pad, 0.0)
if len(starts) > len(ends): # тишина до самого конца файла
if starts[-1] - cur > 0.2:
keep.append((cur, starts[-1] + pad))
elif total - cur > 0.2:
keep.append((cur, total))
if not keep:
sys.exit("Не нашёл речи. Уменьшите noise_db (например -45).")
parts = []
for i, (a, b) in enumerate(keep):
parts.append(
f"[0:v]trim={a:.3f}:{b:.3f},setpts=PTS-STARTPTS[v{i}];"
f"[0:a]atrim={a:.3f}:{b:.3f},asetpts=PTS-STARTPTS[a{i}];")
labels = "".join(f"[v{i}][a{i}]" for i in range(len(keep)))
graph = "".join(parts) + f"{labels}concat=n={len(keep)}:v=1:a=1[v][a]"
with open("_cut_filter.txt", "w", encoding="utf-8") as f:
f.write(graph)
run(["ffmpeg", "-y", "-i", src, "-filter_complex_script", "_cut_filter.txt",
"-map", "[v]", "-map", "[a]", "-c:v", "libx264", "-preset", "fast",
"-crf", "20", "-pix_fmt", "yuv420p", "-c:a", "aac", dst])
print(f"Готово: {dst} (было {total:.0f} с, оставлено фрагментов: {len(keep)})")
def esc_text(t):
return (t.replace("\\", "\\\\").replace("'", "\u2019")
.replace(":", "\\:").replace("%", "\\%"))
def compose(src, cfg_path, dst):
with open(cfg_path, encoding="utf-8") as f:
cfg = json.load(f)
music = cfg.get("music", [])
images = cfg.get("images", [])
texts = cfg.get("texts", [])
font = cfg.get("font", "")
font = font.replace("\\", "/").replace(":", "\\:")
inputs = ["-i", src]
idx = 1
music_idx, image_idx = [], []
for m in music:
inputs += ["-stream_loop", "-1", "-i", m["file"]]
music_idx.append(idx)
idx += 1
for im in images:
inputs += ["-i", im["file"]]
image_idx.append(idx)
idx += 1
g = []
# --- звук ---
if music:
mix = ["[0:a]"]
for k, (m, i) in enumerate(zip(music, music_idx)):
s, e = float(m["start"]), float(m["end"])
vol = float(m.get("volume", 0.15))
fade = min(1.5, (e - s) / 3)
g.append(
f"[{i}:a]atrim=0:{e - s:.3f},asetpts=PTS-STARTPTS,"
f"volume={vol},afade=t=in:d={fade:.2f},"
f"afade=t=out:st={e - s - fade:.3f}:d={fade:.2f},"
f"adelay={int(s * 1000)}|{int(s * 1000)}[m{k}]")
mix.append(f"[m{k}]")
g.append("".join(mix) +
f"amix=inputs={len(mix)}:duration=first:normalize=0[aout]")
else:
g.append("[0:a]anull[aout]")
# --- видео ---
cur = "0:v"
for j, (im, i) in enumerate(zip(images, image_idx)):
w = im.get("width", 500)
x = im.get("x", "(W-w)/2")
y = im.get("y", "(H-h)/2")
g.append(f"[{i}:v]scale={w}:-1[im{j}]")
g.append(f"[{cur}][im{j}]overlay=x={x}:y={y}:"
f"enable='between(t,{im['start']},{im['end']})'[vi{j}]")
cur = f"vi{j}"
for j, t in enumerate(texts):
ff = f"fontfile='{font}':" if font else ""
g.append(
f"[{cur}]drawtext={ff}text='{esc_text(t['text'])}':"
f"fontsize={t.get('size', 64)}:fontcolor=white:"
f"borderw=4:bordercolor=black:x=(w-text_w)/2:"
f"y={t.get('y', 'h*0.8')}:"
f"enable='between(t,{t['start']},{t['end']})'[vt{j}]")
cur = f"vt{j}"
g.append(f"[{cur}]copy[vout]")
with open("_compose_filter.txt", "w", encoding="utf-8") as f:
f.write(";\n".join(g))
run(["ffmpeg", "-y"] + inputs + [
"-filter_complex_script", "_compose_filter.txt",
"-map", "[vout]", "-map", "[aout]",
"-c:v", "libx264", "-preset", "fast", "-crf", "20",
"-pix_fmt", "yuv420p", "-c:a", "aac", dst])
print(f"Готово: {dst}")
if __name__ == "__main__":
if len(sys.argv) == 4 and sys.argv[1] == "cut":
cut_silence(sys.argv[2], sys.argv[3])
elif len(sys.argv) == 5 and sys.argv[1] == "compose":
compose(sys.argv[2], sys.argv[3], sys.argv[4])
else:
print(__doc__)