python+ffmpeg实现视频字幕合并压缩分离裁剪提取片段代码

代码语言:python

所属分类:其他

代码描述:python+ffmpeg实现视频字幕合并压缩分离裁剪提取片段代码,还可将多张图片转成视频。

代码标签: python ffmpeg 视频 字幕 合并 压缩 分离 裁剪 提取 片段 代码

下面为部分代码预览,完整代码请点击下载或在bfwstudio webide中打开

# -*- coding: utf-8 -*-
import os
import sys
import re
import json
import shutil
import tempfile
import threading
import subprocess
import queue
import time
import tkinter as tk
from tkinter import ttk, filedialog, messagebox

APP_TITLE = "FFmpeg 工具箱 - Win10(优化版)"
DEFAULT_VIDEO_EXT = ".mp4"

def which_exe(name):
    return shutil.which(name)

def parse_time_str(s):
    s = s.strip()
    if not s:
        raise ValueError("时间为空")
    if re.match(r"^\d+(\.\d+)?$", s):
        return float(s)
    m = re.match(r"^(?:(\d{1,2}):)?(\d{1,2}):(\d{1,2})(?:\.(\d{1,3}))?$", s)
    if not m:
        raise ValueError(f"时间格式不合法: {s}")
    hh = int(m.group(1) or 0)
    mm = int(m.group(2))
    ss = int(m.group(3))
    ms = int(m.group(4) or 0)
    return hh*3600 + mm*60 + ss + ms/1000.0

def seconds_to_hhmmss(t):
    hh = int(t // 3600)
    mm = int((t % 3600) // 60)
    ss = t - hh*3600 - mm*60
    return f"{hh:02d}:{mm:02d}:{ss:06.3f}"

def escape_subtitles_filter_path(path):
    p = os.path.abspath(path)
    p = p.replace("\\", "/")
    p = p.replace(":", r"\:")
    p = p.replace("'", r"\'")
    return p

def escape_concat_path(path):
    # 给 concat 列表使用:用正斜杠并转义引号
    p = os.path.abspath(path).replace("\\", "/")
    return p.replace("'", "'\\''")

def human_kbps(val):
    """将诸如 '2500k' '2500' '2.5M' 统一成 ffmpeg 接受的码率字符串"""
    s = str(val).strip().lower()
    if s.endswith("k") or s.endswith("m"):
        return s
    if s.endswith("kbps"):
        return s[:-4]
    if s.endswith("mbps"):
        try:
            num = float(s[:-4])
            return f"{int(num*1000)}k"
        except:
            return "2500k"
    # 纯数字默认 kbps
    if re.match(r"^\d+(\.\d+)?$", s):
        if "." in s:
            try:
                f = float(s)
                return f"{int(f)}k"
            except:
                return "2500k"
        return f"{s}k"
    return "2500k"

def file_size(path):
    try:
        return os.path.getsize(path)
    except:
        return -1

class FFmpegGUI(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title(APP_TITLE)
        self.geometry("1050x800")
        self.minsize(980, 720)

        self.ffmpeg_path = which_exe("ffmpeg") or ""
        self.ffprobe_path = which_exe("ffprobe") or ""

        self.log_queue = queue.Queue()
        self.proc = None
        self.proc_thread = None

        self._queued_cmds = []
        self._tmp_concat_file = None
        self._tmp_to_delete = []
        self._comp_guard_ctx = None  # 压缩/字幕大小保护上下文(共用)

        self._build_ui()

    # ---------------- UI ----------------
    def _build_ui(self):
        self.notebook = ttk.Notebook(self)
        self.notebook.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)

        self.tab_subs = ttk.Frame(self.notebook)
        self.tab_compress = ttk.Frame(self.notebook)
        self.tab_demux = ttk.Frame(self.notebook)
        self.tab_images = ttk.Frame(self.notebook)
        self.tab_crop = ttk.Frame(self.notebook)
        self.tab_cut = ttk.Frame(self.notebook)
        self.tab_settings = ttk.Frame(self.notebook)

        self.notebook.add(self.tab_subs, text="字幕硬合并")
        self.notebook.add(self.tab_compress, text="压缩视频(增强)")
        self.notebook.add(self.tab_demux, text="分离音视频")
        self.notebook.add(self.tab_images, text="多图转视频")
        self.notebook.add(self.tab_crop, text="画面裁剪")
        self.notebook.add(self.tab_cut, text="片段提取")
        self.notebook.add(self.tab_settings, text="设置")

        self._build_tab_subs()
        self._build_tab_compress()
        self._build_tab_demux()
        self._build_tab_images()
        self._build_tab_crop()
        self._build_tab_cut()
        self._build_tab_settings()

        # Log + Control
        bottom = ttk.Frame(self)
        bottom.pack(fill=tk.BOTH, expand=False, padx=8, pady=(0,8))

        self.run_btn = ttk.Button(bottom, text="开始执行", command=self.on_run_click)
        self.run_btn.grid(row=0, column=0, padx=4, pady=4, sticky="w")

        self.stop_btn = ttk.Button(bottom, text="停止", command=self.on_stop_click, state=tk.DISABLED)
        self.stop_btn.grid(row=0, column=1, padx=4, pady=4, sticky="w")

        self.clear_btn = ttk.Button(bottom, text="清空日志", command=self.clear_log)
        self.clear_btn.grid(row=0, column=2, padx=4, pady=4, sticky="w")

        # Log text
        log_frame = ttk.Frame(self)
        log_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=(0,8))
        self.log_text = tk.Text(log_frame, height=16, wrap=tk.NONE, font=("Consolas", 10))
        ys = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
        xs = ttk.Scrollbar(log_frame, orient=tk.HORIZONTAL, command=self.log_text.xview)
        self.log_text.configure(yscrollcommand=ys.set, xscrollcommand=xs.set)
        self.log_text.grid(row=0, column=0, sticky="nsew")
        ys.grid(row=0, column=1, sticky="ns")
        xs.grid(row=1, column=0, sticky="ew")
        log_frame.rowconfigure(0, weight=1)
        log_frame.columnconfigure(0, weight=1)

        self.status_var = tk.StringVar(value="就绪")
        self.status = ttk.Label(self, textvariable=self.status_var, anchor="w")
        self.status.pack(fill=tk.X, side=tk.BOTTOM, padx=8, pady=(0,8))

        self.after(100, self._poll_log_queue)

    def _browse_file(self, var, title="选择文件", filetypes=(("所有文件","*.*"),)):
        path = filedialog.askopenfilename(title=title, filetypes=filetypes)
        if path:
            var.set(path)

    def _browse_save_video(self, var, title="保存为", defaultext=DEFAULT_VIDEO_EXT):
        ftypes = (("MP4 视频","*.mp4"), ("MKV 视频","*.mkv"), ("MOV 视频","*.mov"), ("所有文件","*.*"))
        path = filedialog.asksaveasfilename(title=title, defaultextension=defaultext, filetypes=ftypes)
        if path:
            var.set(path)

    def _browse_folder(self, var, title="选择文件夹"):
        path = filedialog.askdirectory(title=title)
        if path:
            var.set(path)

    # ------------- Tab: 字幕硬合并 -------------
    def _build_tab_subs(self):
        f = self.tab_subs
        pad = {"padx": 6, "pady": 6}

        self.subs_in_video = tk.StringVar()
        self.subs_in_sub = tk.StringVar()
        self.subs_out = tk.StringVar()
        # 默认 CRF 提升到 22,避免体积暴涨
        self.subs_crf = tk.IntVar(value=22)
        self.subs_preset = tk.StringVar(value="medium")
        self.subs_copy_audio = tk.BooleanVar(value=True)

        ttk.Label(f, text="输入视频:").grid(row=0, column=0, sticky="e", **pad)
        ttk.Entry(f, textvariable=self.subs_in_video, width=70).grid(row=0, column=1, sticky="we", **pad)
        ttk.Button(f, text="浏览", command=lambda:self._browse_file(self.subs_in_video, "选择视频", (("视频文件","*.mp4 *.mkv *.mov *.avi *.flv *.wmv"),("所有文件","*.*")))).grid(row=0, column=2, **pad)

        ttk.Label(f, text="字幕文件:").grid(row=1, column=0, sticky="e", **pad)
        ttk.Entry(f, textvariable=self.subs_in_sub, width=70).grid(row=1, column=1, sticky="we", **pad)
        ttk.Button(f, text="浏览", command=lambda:self._browse_file(self.subs_in_sub, "选择字幕文件", (("字幕文件","*.srt *.ass *.ssa"),("所有文件","*.*")))).grid(row=1, column=2, **pad)

        ttk.Label(f, text="输出文件:").grid(row=2, column=0, sticky="e", **pad)
        ttk.Entry(f, textvariable=self.subs_out, width=70).grid(row=2, column=1, sticky="we", **pad)
        ttk.Button(f, text="浏览", command=lambda:self._browse_save_video(self.subs_out, "保存硬字幕视频为")).grid(row=2, column=2, **pad)

        ttk.Label(f, text="CRF(0-51):").grid(row=3, column=0, sticky="e", **pad)
        ttk.Spinbox(f, from_=0, to=51, textvariable=self.subs_crf, width=6).grid(row=3, column=1, sticky="w", **pad)

        ttk.Label(f, text="preset:").grid(row=3, column=1, sticky="e", padx=(200,6), pady=6)
        ttk.Combobox(f, values=["ultrafast","superfast","veryfast","faster","fast","medium","slow","slower","veryslow"], textvariable=self.subs_preset, width=10, state="readonly").grid(row=3, column=2, sticky="w", **pad)

        ttk.Checkbutton(f, text="复制音频(c:a copy)", variable=self.subs_copy_audio).grid(row=4, column=1, sticky="w", **pad)

        info = "提示: 需要 ffmpeg 带 libass 的 build;字幕是硬字幕,写入画面,无法关闭。"
        ttk.Label(f, text=info, foreground="#666").grid(row=5, column=0, columnspan=3, sticky="w", **pad)

        for i in range(3):
            f.columnconfigure(i, weight=1)

    # ------------- Tab: 压缩视频(增强) -------------
    def _build_tab_compress(self):
        f = self.tab_compress
        pad = {"padx": 6, "pady": 6}

        self.comp_in = tk.StringVar()
        self.comp_out = tk.StringVar()

        # 新增参数
        self.comp_encoder = tk.StringVar(value="libx264 (H.264 CPU)")
        self.comp_mode = tk.StringVar(value="CRF/CQP")
        self.comp_crf = tk.IntVar(value=23)
        self.comp_preset = tk.StringVar(value="medium")
        self.comp_tune = tk.StringVar(value="none")
        self.comp_bitrate = tk.StringVar(value="2500k")
        self.comp_scale = tk.StringVar(value="保持原分辨率")
        se.........完整代码请登录后点击上方下载按钮下载查看

网友评论0