开发者

基于PyQt6实现智能视频分割器

开发者 https://www.devze.com 2025-05-18 09:20 出处:网络 作者: 创客白泽
目录一、开篇碎碎念二、功能全景图2.1 核心功能清单2.2 技术选型对比三、UI设计详解3.1 色彩方案3.2 关键界面组件四、手把手教学:从安装到使用4.1 环境准备4.2 视频分割处理流程五、核心代码解析5.1 拖拽处理三件套
目录
  • 一、开篇碎碎念
  • 二、功能全景图
    • 2.1 核心功能清单
    • 2.2 技术选型对比
  • 三、UI设计详解
    • 3.1 色彩方案
    • 3.2 关键界面组件
  • 四、手把手教学:从安装到使用
    • 4.1 环境准备
    • 4.2 视频分割处理流程
  • 五、核心代码解析
    • 5.1 拖拽处理三件套
    • 5.2 FFmpeg多线程调用
    • 5.3 智能区间计算
  • 六、源码下载
    • 七、总结与展望
      • 7.1 技术总结
      • 7.2 未来计划

    一、开篇碎碎念

    最近在整理旅行视频时,发现需要把长视频按场景分割成小片段。试了几款工具都不够顺手,要么操作复杂,要么界面丑陋。作为技术人,当然要自己造轮子!

    本文将带你用PyQt6打造一款支持拖拽操作、三种分割模式、高颜值界面的视频分割神器。最重要的是——完整开源!文末提供源码下载~

    (效果预览图)

    基于PyQt6实现智能视频分割器

    基于PyQt6实现智能视频分割器

    基于PyQt6实现智能视频分割器

    基于PyQt6实现智能视频分割器

    二、功能全景图

    2.1 核心功能清单

    功能模块技术实现亮点
    拖拽文件支持Qt6 DnD API拖拽即识别,边框动态反馈
    三种分割模式FFmpeg子进程按秒/按段/区间三种策略
    实时进度展示QProgressBar平滑动画+百分比计算
    智能时长解析FFprobe调用自动识别视频元数据
    跨平台支持python封装Win/MACOS/linux全兼容

    2.2 技术选型对比

    基于PyQt6实现智能视频分割器

    三、UI设计详解

    3.1 色彩方案

    采用蓝绿渐变科技风配色:

    COLOR_SCHEME = {
        'primary': '#5D9CEC',  # 主按钮蓝
        'success': '#4CAF50',  # 成功绿
        'danger': '#F44336',   # 警告红
        'background': '#F0F7FA' # 背景浅蓝
    }
    

    3.2 关键界面组件

    拖拽文件区:虚线边框+悬浮高亮

       QLabel {
           border: 2px dashed #aaa;
           border-radius: 5px;
           background: rgba(255,255,255,0.7);
       }
       QLabel:hover { border-color: #5D9CEC; }
    

    动态进度条:

       self.progress.setStyleSheet("""
           QProgressBar::chunk {
               background: qlineargradient(
                   x1:0, y1:0, x2:1, y2:0,
                   stop:0 #4CAF50, stop:1 #8BC34A);
           }
       """)
    

    四、手把手教学:从安装到使用

    4.1 环境准备

    # 必备依赖
    pip install PyQt6 moviepy
    # FFmpeg安装(Windows示例)
    choco install ffmpeg
    

    4.2 视频分割处理流程

    基于PyQt6实现智能视频分割器

    五、核心代码解析

    5.1 拖拽处理三件套

    def dragEnterEvent(self, event):
        """文件拖入时触发"""
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
    
    def dropEvent(self, event):
        """文件放下时处理"""
        file_path = event.mimeData().urls()[0].toLocalFile()
        self.process_video(file_path)
    

    5.2 FFmpeg多线程调用

    Thread(target=lambda: 
        subprocess.run([
            'ffmpeg', '-ss', start_time,
            '-i', input_path,
            '-t', duration,
            '-c', 'copy', output_path
        ], check=True)
    ).start()
    

    5.3 智能区间计算

    def calculate_intervals(self):
        """自动计算合理分段"""
        duration = self.get_duration()
        if mode == "seconds":
            return [(i*interval, (i+1)*interval) 
                   for i in range(math.ceil(duration/interval))]
    

    六、源码下载

    import os
    import math
    import subprocess
    import json
    import re
    from threading import Thread
    from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QvboxLayout, QHBoxLayout, 
                                QLabel, QPushButton, QLineEdit, QRadioButton, QButtonGroup,
                                QProgressBar, QListWidget, QFileDialog, QMessageBox,
                                QDialog, QFrame, QListWidgetItem)
    from PyQt6.QtCore import Qt, QSize, QMimeData, QUrl
    from PyQt6.QtGui import QIcon, QFont, QColor, QDragEnterEvent, QDropEvent
    
    
    class VideoProcessor:
        def __init__(self):
            self.source = ""
            self.running = True
    
        def set_source(self, path):
            """设置视频源文件"""
            if not os.path.exists(path):
                raise FileNotFoundError("文件不存在")
            self.source = path
    
        def start(self, mode, param, progress_callback, status_callback):
            """处理入口"""
            duration = self.get_duration()
    
            if mode == "seconds":
                self.split_by_seconds(param, duration, progress_callback, status_callback)
            elif mode == "segments":
                self.split_by_segments(param, duration, progress_callback, status_callback)
            elif mode == "intervals":
                self.split_by_intervals(param, duration, progress_callback, status_callback)
            else:
                raise ValueError(f"不支持的分割模式: {mode}")
    
        def split_by_seconds(self, interval, duration, progress_callback, status_callback):
            """秒级分割逻辑"""
            full_segments = int(duration // interval)
            remaining = duration % interval
    
            actual_segments = full_segments + (1 if remaining >= 1 else 0)
            output_dir = self.create_output_dir("秒分割")
    
            base_name, ext = os.path.splitext(os.path.basename(self.source))
            for i in range(actual_segments):
                if not self.running:
                    break
                start = i * interval
                end = (i + 1) * interval if i < actual_segments - 1 else duration
    
                output_name = f"{base_name}_副本_{i + 1}{ext}"
                subprocess.run([
                    'ffmpeg', '-y', '-ss', str(start), '-i', self.source,
                    '-t', str(end - start), '-c', 'copy',
                    os.path.join(output_dir, output_name)
                ], check=True)
                progress_callback((i + 1) / actual_segments * 100)
                status_callback(f"生成分段 {i + 1}/{actual_segments}")
    
        def split_by_segments(self, count, duration, progress_callback, status_callback):
            """分段模式处理"""
            interval = duration / count
            output_dir = self.create_output_dir("段分割")
    
            base_name, ext = os.path.splitext(os.path.basename(self.source))
            for i in range(count):
                if not self.running:
                    break
                start = i * interval
                end = duration if i == count - 1 else (i + 1) * interval
    
                output_name = f"{base_name}_副本_{i + 1}{ext}"
                subprocess.run([
                    'ffmpeg', '-y', '-ss', str(start), '-i', self.source,
                    '-t', str(end - start), '-c', 'copy',
                    os.path.join(output_dir, output_name)
                ], check=True)
                progress_callback((i + 1) / count * 100)
                status_callback(f"处理进度 {i + 1}/{count}")
    
        def split_by_intervals(self, intervals, duration, progress_callback, status_callback):
            """区间提取逻辑"""
            valid_intervals = []
            for interval in intervals:
                start, end = interval
                if 0 <= start < end <= duration and end - start >= 0.5:
                    valid_intervals.append((start, end))
    
            if not valid_intervals:
                raise ValueError("没有有效的提取区间")
    
            output_dir = self.create_output_dir("区间提取")
    
            base_name, ext = os.path.splitext(os.path.basename(self.source))
            total = len(valid_intervals)
            for i, (start, end) in enumerate(valid_intervals):
                if not self.running:
                    break
    
                start_hour, start_minute, start_second = self.seconds_to_hms(start)
                end_hour, end_minute, end_second = self.seconds_to_hms(end)
                output_name = f"{base_name}_副本_{i + 1}_{start_hour:02d}{start_minute:02d}{start_second:02d}-{end_hour:02d}{end_minute:02d}{end_second:02d}{ext}"
                subprocess.run([
                    'ffmpeg', '-y', '-ss', str(start), '-i', self.source,
                    '-t', str(end - start), '-c', 'copy',
                    os.path.join(output_dir, output_name)
                ], check=True)
    
                progress_callback((i + 1) / total * 100)
                status_callback(f"提取区间 {i + 1}/{total}: {start_hour:02d}:{start_minute:02d}:{start_second:02d}-{end_hour:02d}:{end_minute:02d}:{end_second:02d}")
    
            return output_dir
    
        def get_duration(self):
            """获取视频时长"""
            try:
                cmd = [
                    'ffprobe', '-v', 'error', '-show_entries', 
                    'format=duration', '-of', 'json', self.source
                ]
                result = subprocess.run(cmd, capture_output=True, text=True, check=True)
                data = json.loads(result.stdout)
                duration = float(data['format']['duration'])
                return duration
            except subprocess.CalledProcessError as e:
                raise RuntimeError(f"读取时长失败: {e.stderr}")
            except (json.JSONDecodeError, KeyError, ValueError) as e:
                raise RuntimeError(f"解析视频信息失败: {str(e)}")
            except Exception as e:
                raise RuntimeError(f"读取时长失败: {str(e)}")
    
        def create_output_dir(self, prefix):
            """创建输出目录"""
            output_dir = os.path.join(os.path.dirname(self.source), f"{prefix}_输出")
            os.makedirs(output_dir, exist_ok=True)
            return output_dir
    
        def seconds_to_hms(self, seconds):
            """将秒转换为小时、分钟、秒"""
            hours = int(seconds // 3600)
            minutes = int((seconds % 3600) // 60)
            secs = int(seconds % 60)
            return hours, minutes, secs
    
        def hms_to_seconds(self, hours, minutes, seconds):
            """将小时、分钟、秒转换为秒"""
            return hours * 3600 + minutes * 60 + seconds
    
    
    class AddIntervalDialog(QDialog):
        def __init__(self, parent, max_duration):
            super().__init__(parent)
            self.setWindowTitle("添加区间")
            self.setWindowModality(Qt.WindowModality.ApplicationModal)
            self.setFixedSize(350, 200)
            
            self.max_duration = max_duration
            self.result = None
            
            self.init_ui()
            
        def init_ui(self):
            layout = QVBoxLayout()
            layout.setContentsMargins(15, 15, 15, 15)
            
            info_label = QLabel(f"请输入时间区间 (范围: 0-{self.format_time(self.max_duration)})")
            info_label.setStyleSheet("color: #555;")
            layout.addwidget(info_label)
            
            # 开始时间输入
            start_frame = QWidget()
            start_layout = QHBoxLayout(start_frame)
            start_layout.setContentsMargins(0, 0, 0, 0)
            
            start_layout.addWidget(QLabel("开始时间:"))
            self.start_hour = QLineEdit()
            self.start_hour.setFixedWidth(30)
            self.start_hour.setStyleSheet("background: #f8f8f8;")
            start_layout.addWidget(self.start_hour)
            start_layout.addWidget(QLabel("时"))
            
            self.start_minute = QLineEdit()
            self.start_minute.setFixedWidth(30)
            self.start_minute.setStyleSheet("background: #f8f8f8;")
            start_layout.addWidget(self.start_minute)
            start_layout.addWidget(QLabel("分"))
            
            self.start_second = QLineEdit()
            self.start_second.setFixedWidth(30)
            self.start_second.setStyleSheet("background: #f8f8f8;")
            start_layout.addWidget(self.start_second)
            start_layout.addWidget(QLabel("秒"))
            
            start_layout.addStretch()
            layout.addWidget(start_frame)
            
            # 结束时间输入
            end_frame = QWidget()
            end_layout = QHBoxLayout(end_frame)
            end_layout.setContentsMargins(0, 0, 0, 0)
            
            end_layout.addWidget(QLabel("结束时间:"))
            self.end_hour = QLineEdit()
            self.end_hour.setFixedWidth(30)
            self.end_hour.setStyleSheet("background: #f8f8f8;")
            end_layout.addWidget(self.end_hour)
            end_layout.addWidget(QLabel("时"))
            
            self.end_minute = QLineEdit()
            self.end_minute.setFixedWidth(30)
            self.end_minute.setStyleSheet("background: #f8f8f8;")
            end_layout.addWidget(self.end_minute)
            end_layout.addWidget(QLabel("分"))
            
            self.end_second = QLineEdit()
            self.end_second.setFixedWidth(30)
            self.end_second.setStyleSheet("background: #f8f8f8;")
            end_layout.addWidget(self.end_second)
            end_layout.addWidget(QLabel("秒"))
            
            end_layout.addStretch()
            layout.addWidget(end_frame)
            
            # 按钮区
            btn_frame = QWidget()
            btn_layout = QHBoxLayout(btn_frame)
            btn_layout.setCojsntentsMargins(0, 0, 0, 0)
            
            self.ok_btn = QPushButton("确定")
            self.ok_btn.setStyleSheet("""
                QPushButton {
                    background: #4CAF50;
                    color: white;
                    border: none;
                    padding: 5px 15px;
                    border-radius: 4px;
                }
                QPushButton:hover {
                    background: #45a049;
                }
            """)
            self.ok_btn.clicked.connect(self.on_ok)
            btn_layout.addStretch()
            btn_layout.addWidget(self.ok_btn)
            
            cancel_btn = QPushButton("取消")
            cancel_btn.setStyleSheet("""
                QPushButton {
                    background: #f44336;
                    color: white;
                    border: none;
                    padding: 5px 15px;
                    border-radius: 4px;
                }
                QPushButton:hover {
                    background: #d32f2f;
                }
            """)
            cancel_btn.clicked.connect(self.reject)
            btn_layout.addWidget(cancel_btn)
            
            layout.addWidget(btn_frame)
            
            self.setLayout(layout)
            
        def on_ok(self):
            try:
                start_h = int(self.start_hour.text() or 0)
                start_m = int(self.start_minute.text() or 0)
                start_s = int(self.start_second.text() or 0)
                
                end_h = int(self.end_hour.text() or 0)
                end_m = int(self.end_minute.text() or 0)
                end_s = int(self.end_second.text() or 0)
                
                start_seconds = start_h * 3600 + start_m * 60 + start_s
                end_seconds = end_h * 3600 + end_m * 60 + end_s
                
                if start_seconds < 0 or start_seconds >= self.max_duration:
                    QMessageBox.warning(self, "错误", f"开始时间必须在0到{self.format_time(self.max_duration)}之间")
                    return
                    
                if end_seconds <= start_seconds or end_seconds > self.max_duration:
                    QMessageBox.warning(self, "错误", f"结束时间必须大于开始时间且不超过{self.format_time(self.max_duration)}")
                    return
                    
                if end_seconds - start_seconds < 0.5:
                    QMessageBox.warning(self, "错误", "区间长度太短(最少0.5秒)")
                    return
                    
                self.result = (start_seconds, end_seconds)
                self.accept()
                
            except ValueError:
                QMessageBox.warning(self, "错误", "请输入有效的数字")
        
        def format_time(self, seconds):
            """将秒转换为 hh:mm:ss 格式"""
            hours = int(seconds // 3600)
            minutes = int((seconds % 3600) // 60)
            secs = int(seconds % 60)
            return f"{hours:02d}:{minutes:02d}:{secs:02d}"
    
    
    class VideoSplitterUI(QMainWindow):
        def __init__(self):
            super().__init__()
            self.setWindowTitle("智能视频分割器")
            self.setMinimumSize(650, 600)
            
            self.processor = VideoProcessor()
            self.running = False
            self.intervals = []
            
            self.init_ui()
            self.center_window()
            
            # 启用拖放功能
            self.setAcceptDrops(True)
            
        def center_window(self):
            """使窗口居中显示"""
            screen = QApplication.primaryScreen().geometry()
            size = self.geometry()
            self.move((screen.width() - size.width()) // 2, 
                     (screen.height() - size.height()) // 2)
            
        def init_ui(self):
            """初始化UI界面"""
            main_widget = QWidget()
            self.setCentralWidget(main_widget)
            
            # 主布局
            layout = QVBoxLayout()
            layout.setContentsMargins(25, 25, 25, 25)
            layout.setSpacing(15)
            
            # 文件选择区
            file_frame = QWidget()
            file_frame.setStyleSheet("""
                QWidget {
                    background: #f0f7fa;
                    border-radius: 8px;
                    padding: 10px;
                }
            """)
            file_layout = QHBoxLayout(file_frame)
            file_layout.setContentsMargins(10, 5, 10, 5)
            
            self.select_btn = QPushButton("选择文件")
            self.select_btn.setFixedWidth(100)
            self.select_btn.setStyleSheet("""
                QPushButton {
                    background: #5D9CEC;
                    color: white;
                    border: none;
                    padding: 6px;
                    border-radius: 5px;
                    font-weight: bold;
                }
                QPushButton:hover {
                    background: #4A89DC;
                }
            """)
            self.select_btn.clicked.connect(self.select_file)
            file_layout.addWidget(self.select_btn)
            
            self.file_label = QLabel("拖放视频文件到这里或点击按钮选择")
            self.file_label.setStyleSheet("""
                QLabel {
                    color: #555;
                    font-size: 13px;
                    padding: 5px;
                    border: 2px dashed #aaa;
                    border-radius: 5px;
                    background: rgba(255,255,255,0.7);
                }
            """)
            self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
            file_layout.addWidget(self.file_label, 1)
            
            layout.addWidget(file_frame)
            
            # 视频信息区
            self.info_label = QLabel("视频时长: 未知")
            self.info_label.setStyleSheet("""
                QLabel {
                    color: #4A708B;
                    font-size: 13px;
                    font-weight: bold;
                    padding: 5px;
                    background: #E0F2F7;
                    border-radius: 5px;
                }
            """)
            self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
            layout.addWidget(self.info_label)
            
            # 分割线
            line = QFrame()
            line.setFrameShape(QFrame.Shape.HLine)
            line.setFrameShadow(QFrame.Shadow.Sunken)
            line.setStyleSheet("color: #d5e6eb;")
            layout.addWidget(line)
            
            # 模式选择区
            mode_frame = QWidget()
            mode_frame.setStyleSheet("""
                QWidget {
                    background: #f5f9fa;
                    border-radius: 8px;
                    padding: 10px;
                }
            """)
            mode_layout = QHBoxLayout(mode_frame)
            mode_layout.setContentsMargins(15, 5, 15, 5)
            
            self.mode_group = QButtonGroup()
            
            self.secouDNDTNtRnds_radio = QRadioButton("按秒分割")
            self.seconds_radio.setChecked(True)
            self.seconds_radio.setStyleSheet("""
                QRadioButton {
                    color: #555;
                    font-size: 13px;
                }
                QRadioButton::indicator {
                    width: 16px;
                    height: 16px;
                }
            """)
            self.mode_group.addButton(self.seconds_radio, 0)
            mode_layout.addWidget(self.seconds_radio)
            
            self.segments_radio = QRadioButton("按段分割")
            self.segments_radio.setStyleSheet("""
                QRadioButton {
                    color: #555;
                    font-size: 13px;
                }
                QRadioButton::indicator {
                    width: 16px;
                    height: 16px;
                }
            """)
            self.mode_group.addButton(self.segments_radio, 1)
            mode_layout.addWidget(self.segments_radio)
            
            self.intervals_radio = QRadioButton("区间提取")
            self.intervals_radio.setStyleSheet("""
                QRadioButton {
                    color: #555;
                    font-size: 13px;
                }
                QRadioButton::indicator {
                    width: 16px;
                    height: 16px;
                }
            """)
            self.mode_group.addButton(self.intervals_radio, 2)
            mode_layout.addWidget(self.intervals_radio)
            
            mode_layout.addStretch()
            layout.addWidget(mode_frame)
            
            # 参数输入区
            self.param_frame = QWidget()
            self.param_frame.setStyleSheet("""
                QWidget {
                    background: #f5f9fa;
                    border-radius: 8px;
                    padding: 10px;
                }
            """)
            param_layout = QHBoxLayout(self.param_frame)
            param_layout.setContentsMargins(15, 5, 15, 5)
            
            self.param_label = QLabel("间隔秒数:")
            self.param_label.setStyleSheet("color: #555;")
            param_layout.addWidget(self.param_label)
            
            self.param_entry = QLineEdit()
            self.param_entry.setFixedWidth(80)
            self.param_entry.setStyleSheet("""
                QLineEdit {
                    background: white;
                    border: 1px solid #ccc;
                    border-radius: 4px;
                    padding: 3px;
                }
            """)
            self.param_entry.textChanged.connect(self.auto_calculate)
            param_layout.addWidget(self.param_entry)
            
            self.result_label = QLabel("预计段数: 0")
            self.result_label.setStyleSheet("color: #666; font-weight: bold;")
            param_layout.addWidget(self.result_label)
            param_layout.addStretch()
            
            layout.addWidget(self.param_frame)
            
            # 区间提取区
            self.intervals_frame = QWidget()
            self.intervals_frame.setStyleSheet("""
                QWidget {
                    background: #f5f9fa;
                    border-radius: 8px;
                    padding: 10px;
                }
            """)
            intervals_layout = QVBoxLayout(self.intervals_frame)
            intervals_layout.setContentsMargins(10, 10, 10, 10)
            
            intervals_title = QLabel("区间提取")
            intervals_title.setStyleSheet("""
                QLabel {
                    font-weight: bold;
                    color: #4A708B;
                    font-size: 14px;
                }
            """)
            intervals_layout.addWidget(intervals_title)
            
            # 区间列表
            self.intervals_list = QListWidget()
            self.intervals_list.setMinimumHeight(120)
            self.intervals_list.setStyleSheet("""
                QListWidget {
                    background: white;
                    border: 1px solid #ccc;
                    border-radius: 5px;
                    padding: 5px;
                    font-size: 12px;
                }
                QListWidget::item {
                    padding: 5px;
                    border-bottom: 1px solid #eee;
                }
                QListWidget::item:selected {
                    background: #D6EAF8;
                    color: #333;
                }
            """)
            intervals_layout.addWidget(self.intervals_list)
            
            # 区间控制按钮
            intervals_btn_frame = QWidget()
            intervals_btn_layout = QHBoxLayout(intervals_btn_frame)
            intervals_btn_layout.setContentsMargins(0, 0, 0, 0)
            
            self.add_interval_btn = QPushButton("添加区间")
            self.add_interval_btn.setStyleSheet("""
                QPushButton {
                    background: #5D9CEC;
                    color: white;
                    border: none;
                    padding: 5px 12px;
                    border-radius: 4px;
                }
                QPushButton:hover {
                    background: #4A89DC;
                }
            """)
            self.add_interval_btn.clicked.connect(self.add_interval)
            intervals_btn_layout.addWidget(self.add_interval_btn)
            
            self.remove_interval_btn = QPushButton("删除选中")
            self.remove_interval_btn.setStyleSheet("""
                QPushButton {
                    background: #F78181;
                    color: white;
                    border: none;
                    padding: 5px 12px;
                    border-radius: 4px;
                }
                QPushButton:hover {
                    background: #E74C3C;
                }
            """)
            self.remove_interval_btn.clicked.connect(self.remove_interval)
            intervals_btn_layout.addWidget(self.remove_interval_btn)
            
            self.clear_interval_btn = QPushButton("清空列表")
            self.clear_interval_btn.setStyleSheet("""
                QPushButton {
                    background: #A4A4A4;
                    color: white;
                    border: none;
                    padding: 5px 12px;
                    border-radius: 4px;
                }
                QPushButton:hover {
                    background: #848484;
                }
            """)
            self.clear_interval_btn.clicked.connect(self.clear_intervals)
            intervals_btn_layout.addWidget(self.clear_interval_btn)
            
            intervals_btn_layout.addStretch()
            intervals_layout.addWidget(intervals_btn_frame)
            
            layout.addWidget(self.intervals_frame)
            self.intervals_frame.hide()
            
            # 控制区
            ctrl_frame = QWidget()
            ctrl_frame.setStyleSheet("""
                QWidget {
                    background: #f0f7fa;
                    border-radius: 8px;
                    padding: 10px;
                }
            """)
            ctrl_layout = QHBoxLayout(ctrl_frame)
            ctrl_layout.setContentsMargins(10, 5, 10, 5)
            
            self.start_btn = QPushButton("开始分割")
            self.start_btn.setFixedWidth(120)
            self.start_btn.setStyleSheet("""
                QPushButton {
                    background: #4CA编程客栈F50;
                    color: white;
                    border: none;
                    padding: 8px;
                    border-radius: 5px;
                    font-weight: bold;
                    font-size: 14px;
                }
                QPushButton:hover {
                    background: #45a049;
                }
            """)
            self.start_btn.clicked.connect(self.toggle_process)
            ctrl_layout.addWidget(self.start_btn)
            
            self.open_output_btn = QPushButton("打开输出目录")
            self.open_output_btn.setStyleSheet("""
                QPushButton {
                    background: #5D9CEC;
                    color: white;
                    border: none;
                    padding: 6px 12px;
                    border-radius: 5px;
                }
                QPushButton:hover {
                    background: #4A89DC;
                }
            """)
            self.open_output_btn.clicked.connect(self.open_output)
            ctrl_layout.addWidget(self.open_output_btn)
            
            ctrl_layout.addStretch()
            layout.addWidget(ctrl_frame)
            
            # 进度条
            self.progress = QProgressBar()
            self.progress.setRange(0, 100)
            self.progress.setTextVisible(False)
            self.progress.setStyleSheet("""
                QProgressBar {
                    border: 1px solid #ccc;
                    border-radius: 5px;
                    text-align: center;
                    background: #f5f5f5;
                    height: 15px;
                }
                QProgressBar::chunk {
                    background: #4CAF50;
                    border-radius: 4px;
                }
            """)
            layout.addWidget(self.progress)
            
            # 状态栏
            self.status_label = QLabel("就绪")
            self.status_label.setStyleSheet("""
                QLabel {
                    color: #666;
                    font-size: 12px;
                    padding: 5px;
                    background: #f5f5f5;
                    border-radius: 5px;
                }
            """)
            layout.addWidget(self.status_label)
            
            # 连接信号
            self.mode_group.buttonClicked.connect(self.on_mode_changed)
            
            main_widget.setLayout(layout)
        
        def dragEnterEvent(self, event: QDragEnterEvent):
            """拖拽进入事件"""
            if event.mimeData().hasUrls():
                urls = event.mimeData().urls()
                if len(urls) == 1 and urls[0].isLocalFile():
                    file_path = urls[0].toLocalFile()
                    if re.search(r'\.(mp4|avi|mkv|mov|flv|wmv)$', file_path, re.I):
                        event.acceptProposedAction()
                        self.file_label.setStyleSheet("""
                            QLabel {
                                color: #555;
                                font-size: 13px;
                                padding: 5px;
                                border: 2px dashed #5D9CEC;
                                border-radius: 5px;
                                background: rgba(93, 156, 236, 0.1);
                            }
                        """)
                        return
            
            event.ignore()
        
        def dragLeaveEvent(self, event):
            """拖拽离开事件"""
            self.file_label.setStyleSheet("""
                QLabel {
                    color: #555;
                    font-size: 13px;
                    padding: 5px;
                    border: 2px dashed #aaa;
                    border-radius: 5px;
                    background: rgba(255,255,255,0.7);
                }
            """)
        
        def dropEvent(self, event: QDropEvent):
            """拖放事件"""
            self.file_label.setStyleSheet("""
                QLabel {
                    color: #555;
                    font-size: 13px;
                    padding: 5px;
                    border: 2px dashed #aaa;
                    border-radius: 5px;
                    background: rgba(255,255,255,0.7);
                }
            """)
            
            urls = event.mimeData().urls()
            if urls and urls[0].isLocalFile():
                file_path = urls[0].toLocalFile()
                if re.search(r'\.(mp4|avi|mkv|mov|flv|wmv)$', file_path, re.I):
                    try:
                        self.processor.set_source(file_path)
                        self.file_label.setText(os.path.basename(file_path))
                        
                        duration = self.processor.get_duration()
                        mins, secs = divmod(duration, 60)
                        hours, mins = divmod(mins, 60)
                        time_str = f"{int(hours)}:{int(mins):02d}:{secs:.2f}" if hours else f"{int(mins)}:{secs:.2f}"
                        self.info_label.setText(f"视频时长: {time_str} ({duration:.2f}秒)")
                        
                        self.auto_calculate()
                    except Exception as e:
                        QMessageBox.critical(self, "错误", str(e))
                else:
                    QMessageBox.warning(self, "错误", "请拖入有效的视频文件 (MP4, AVI, MKV, MOV, FLV, WMV)")
        
        def select_file(self):
            """选择视频文件"""
            path, _ = QFileDialog.getOpenFileName(
                self, "选择视频文件", "", 
                "视频文件 (*.mp4 *.avi *.mkv *.mov *.flv *.wmv)"
            )
            
            if path:
                try:
                    self.processor.set_source(path)
                    self.file_label.setText(os.path.basename(path))
                    
                    duration = self.processor.get_duration()
                    mins, secs = divmod(duration, 60)
                    hours, mins = divmod(mins, 60)
                    time_str = f"{int(hours)}:{int(mins):02d}:{secs:.2f}" if hours else f"{int(mins)}:{secs:.2f}"
                    self.info_label.setText(f"视频时长: {time_str} ({duration:.2f}秒)")
                    
                    self.auto_calculate()
                except Exception as e:
                    QMessageBox.critical(self, "错误", str(e))
        
        def on_mode_changed(self, button):
            """模式变化时更新UI"""
            if button == self.seconds_radio:
                self.param_label.setText("间隔秒数:")
                self.param_frame.show()
                self.intervals_frame.hide()
            elif button == self.segments_radio:
                self.param_label.setText("分割为段数:")
                self.param_frame.show()
                self.intervals_frame.hide()
            else:
                self.param_frame.hide()
                self.intervals_frame.show()
                
            self.auto_calculate()
        
        def auto_calculate(self):
            """自动计算分割参数预览"""
            try:
                if not self.processor.source:
                    self.result_label.setText("请先选择文件")
                    return
                    
                if self.seconds_radio.isChecked():
                    if not self.param_entry.text().strip():
                        self.result_label.setText("请输入秒数")
                        return
                        
                    param = float(self.param_entry.text())
                    if param <= 0:
                        self.result_label.setText("请输入大于0的值")
                        return
                        
                    duration = self.processor.get_duration()
                    segments = math.ceil(duration / param)
                    self.result_label.setText(f"预计分成: {segments}段")
                    
                elif self.segments_radio.isChecked():
                    if not self.param_entry.text().strip():
                        self.result_label.setText("请输入段数")
                        return
                        
                    param = float(self.param_entry.text())
                    if param <= 0:
                        self.result_label.setText("请输入大于0的值")
                        return
                        
                    duration = self.processor.get_duration()
                    seconds = duration / param
                    self.result_label.setText(f"每段约: {seconds:.1f}秒")
                    
                else:  # 区间模式
                    count = len(self.intervals)
                    if count == 0:
                        self.result_label.setText("未设置区间")
                    else:
                        total_duration = sum(end - start for start, end in self.intervals)
                        self.result_label.setText(f"将提取: {count}个区间 (共{total_duration:.1f}秒)")
                        
            except ValueError:
                self.result_label.setText("请输入有效数字")
            except Exception as e:
                self.result_label.setText(f"计算错误: {str(e)[:15]}")
        
        def add_interval(self):
            """添加时间区间"""
            if not self.processor.source:
                QMessageBox.warning(self, "错误", "请先选择视频文件")
                return
                
            try:
                duration = self.processor.get_duration()
                dialog = AddIntervalDialog(self, duration)
                
                if dialog.exec() == QDialog.DialogCode.Accepted:
                    self.intervals.append(dialog.result)
                    self.update_intervals_list()
                    self.auto_calculate()
                    
            except Exception as e:
                QMessageBox.critical(self, "错误", str(e))
        
        def remove_interval(self):
            """删除选中的区间"""
            selected = self.intervals_list.currentRow()
            if selected >= 0 and selected < len(self.intervals):
                del self.intervals[selected]
                self.update_intervals_list()
                self.auto_calculate()
        
        def clear_intervals(self):
            """清空区间列表"""
            self.intervals = []
            self.update_intervals_list()
            self.auto_calculate()
        
        def update_intervals_list(self):
            """更新区间列表显示"""
            self.intervals_list.clear()
            for i, (start, end) in enumerate(self.intervals):
                start_h, start_m, start_s = self.processor.seconds_to_hms(start)
                end_h, end_m, end_s = self.processor.seconds_to_hms(end)
                duration = end - start
                item = QListWidgetItem(
                    f"{i + 1}. {start_h:02d}:{start_m:02d}:{start_s:02d} - "
                    f"{end_h:02d}:{end_m:02d}:{end_s:02d} (时长: {duration:.2f}s)"
                )
                self.intervals_list.addItem(item)
        
        def toggle_process(self):
            """处理控制开关"""
            if not self.running:
                if self.validate_params():
                    self.running = True
                    self.start_btn.setText("停止")
                    self.start_btn.setStyleSheet("""
                        QPushButton {
                            background: #f44336;
                            color: white;
                            border: none;
                            padding: 8px;
                            border-radius: 5px;
                            font-weight: bold;
                            font-size: 14px;
                        }
                        QPushButton:hover {
                            background: #d32f2f;
                        }
                    """)
                    Thread(target=self.execute_processing).start()
            else:
                self.running = False
                self.processor.running = False
                self.update_status("操作中止", "#f44336")
        
        def validate_params(self):
            """验证参数输入有效性"""
            if not self.processor.source:
                QMessageBox.warning(self, android"错误", "请先选择视频文件")
                return False
                
            if self.seconds_radio.isChecked() or self.segments_radio.isChecked():
                try:
                    param = float(self.param_entry.text())
                    if param <= 0:
                        QMessageBox.warning(self, "错误", "请输入大于0的数值")
                        return False
                except ValueError:
                    QMessageBox.warning(self, "错误", "请输入有效的数字")
                    return False
            else:  # 区间模式
                if not self.intervals:
                    QMessageBox.warning(self, "错误", "请添加至少一个提取区间")
                    return False
                    
            return True
        
        def execute_processing(self):
            """调用核心处理"""
            try:
                if self.seconds_radio.isChecked():
                    mode = "seconds"
                    param = float(self.param_entry.text())
                elif self.segments_radio.isChecked():
                    mode = "segments"
                    param = int(float(self.param_entry.text()))
                else:
                    mode = "intervals"
                    param = self.intervals
                    
                params = {
                    'mode': mode,
                    'param': param,
                    'progress_callback': self.update_progress,
                    'status_callback': self.update_status
                }
                
                self.processor.running = True
                self.processor.start(**params)
                self.update_status("完成", "#4CAF50")
                
            except Exception as e:
                QMessageBox.critical(self, "错误", str(e))
            http://www.devze.comfinally:
                self.running = False
                self.start_btn.setText("开始分割")
                self.start_btn.setStyleSheet("""
                    QPushButton {
                        background: #4CAF50;
                        color: white;
                        border: none;
                        padding: 8px;
                        border-radius: 5px;
                        font-weight: bold;
                        font-size: 14px;
                    }
                    QPushButton:hover {
                        background: #45a049;
                    }
                """)
        
        def update_progress(self, value):
            """更新进度条"""
            self.progress.setValue(int(value))
        
        def update_status(self, text, color=None):
            """更新状态文本"""
            self.status_label.setText(text)
            if color:
                self.status_label.setStyleSheet(f"""
                    QLabel {{
                        color: {color};
                        font-size: 12px;
                        padding: 5px;
                        background: #f5f5f5;
                        border-radius: 5px;
                    }}
                """)
            else:
                self.status_label.setStyleSheet("""
                    QLabel {
                        color: #666;
                        font-size: 12px;
                        padding: 5px;
                        background: #f5f5f5;
                        border-radius: 5px;
                    }
                """)
        
        def open_output(self):
            """打开输出目录"""
            if not self.processor.source:
                QMessageBox.warning(self, "错误", "请先选择视频文件")
                return
                
            try:
                base_dir = os.path.dirname(self.processor.source)
                if self.seconds_radio.isChecked():
                    prefix = "秒分割"
                elif self.segments_radio.isChecked():
                    prefix = "段分割"
                else:
                    prefix = "区间提取"
                    
                output_dir = os.path.join(base_dir, f"{prefix}_输出")
                
                if not os.path.exists(output_dir):
                    os.makedirs(output_dir, exist_ok=True)
                    self.update_status("创建输出目录", "#5D9CEC")
                    
                if os.name == 'nt':
                    os.startfile(output_dir)
                elif os.name == 'posix':
                    subprocess.call(('xdg-open' if os.uname().sysname == 'Linux' else 'open', output_dir))
                    
                self.update_status(f"已打开: {os.path.basename(output_dir)}", "#5D9CEC")
                
            except Exception as e:
                QMessageBox.critical(self, "错误", f"无法打开输出目录: {str(e)}")
    
    
    if __name__ == "__main__":
        app = QApplication([])
        
        # 设置应用样式
        app.setStyle("Fusion")
        
        # 创建并使用自定义字体
        font = QFont()
        font.setFamily("Microsoft YaHei" if os.name == 'nt' else "PingFang SC")
        font.setPointSize(10)
        app.setFont(font)
        
        # 设置调色板
        palette = app.palette()
        palette.setColor(palette.ColorRole.Window, QColor(240, 247, 250))
        palette.setColor(palette.ColorRole.WindowText, QColor(53, 53, 53))
        palette.setColor(palette.ColorRole.Base, QColor(255, 255, 255))
        palette.setColor(palette.ColorRole.AlternateBase, QColor(240, 247, 250))
        palette.setColor(palette.ColorRole.ToolTipBase, QColor(255, 255, 255))
        palette.setColor(palette.ColorRole.ToolTipText, QColor(53, 53, 53))
        palette.setColor(palette.ColorRole.Text, QColor(53, 53, 53))
        palette.setColor(palette.ColorRole.Button, QColor(240, 247, 250))
        palette.setColor(palette.ColorRole.ButtonText, QColor(53, 53, 53))
        palette.setColor(palette.ColorRole.BrightText, QColor(255, 255, 255))
        palette.setColor(palette.ColorRole.Highlight, QColor(93, 156, 236))
        palette.setColor(palette.ColorRole.HighlightedText, QColor(255, 255, 255))
        app.setPalette(palette)
        
        window = VideoSplitterUI()
        window.show()
        app.exec()
    

    七、总结与展望

    7.1 技术总结

    • Qt6的拖拽API真香!
    • FFmpeg处理视频稳定高效
    • 多线程+信号槽让UI保持流畅

    7.2 未来计划

    • 云端视频处理
    • AI智能分段
    • 插件系统

    到此这篇关于基于PyQt6实现智能视频分割器的文章就介绍到这了,更多相关PyQt6视频分割内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号