import sys
import subprocess
import os
from dataclasses import dataclass
from typing import List, Optional
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QLineEdit, QFileDialog, QProgressBar,
QTextEdit, QGroupBox, QMessageBox
)
from PySide6.QtCore import Qt, Signal, QObject, QRunnable, QThreadPool
from PySide6.QtGui import QFont
@dataclass
class SubtitleSegment:
start_time: str
end_time: str
text: str
class ClipSignals(QObject):
progress = Signal(int)
log = Signal(str)
finished = Signal()
error = Signal(str)
class ClipTask(QRunnable):
def __init__(self, video_path: str, output_dir: str, segments: List[SubtitleSegment], signals: ClipSignals):
super().__init__()
self.video_path = video_path
self.output_dir = output_dir
self.segments = segments
self.signals = signals
self.is_running = True
def run(self):
try:
total = len(self.segments)
for idx, segment in enumerate(self.segments):
if not self.is_running:
break
output_filename = f"clip_{idx+1}_{segment.start_time.replace(':', '-').replace('.', '_')}.mp4"
output_path = os.path.join(self.output_dir, output_filename)
cmd = [
"ffmpeg", "-i", self.video_path, "-ss", segment.start_time,
"-to", segment.end_time, "-c:v", "copy", "-c:a", "copy", "-y", output_path
]
self.signals.log.emit(f"正在剪辑片段 {idx+1}/{total}:{segment.text}")
subprocess.run(cmd, check=True, capture_output=True, text=True)
progress = int(((idx + 1) / total) * 100)
self.signals.progress.emit(progress)
if self.is_running:
self.signals.log.emit("所有片段剪辑完成!")
else:
self.signals.log.emit("剪辑任务已取消!")
except Exception as e:
self.signals.error.emit(f"剪辑失败:{str(e)}")
finally:
self.signals.finished.emit()
def stop(self):
self.is_running = False
class VideoClipWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("视频批量剪辑工具")
self.resize(800, 600)
self.clip_task: Optional[ClipTask] = None
self.signals = ClipSignals()
self.init_ui()
self.bind_signals()
def init_ui(self):
font = QFont("SimHei", 10)
QApplication.setFont(font)
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
file_group = QGroupBox("文件设置")
file_layout = QVBoxLayout(file_group)
video_layout = QHBoxLayout()
self.video_label = QLabel("视频文件:")
self.video_edit = QLineEdit()
self.video_btn = QPushButton("选择")
self.video_btn.clicked.connect(self.select_video)
video_layout.addWidget(self.video_label)
video_layout.addWidget(self.video_edit)
video_layout.addWidget(self.video_btn)
file_layout.addLayout(video_layout)
subtitle_layout = QHBoxLayout()
self.subtitle_label = QLabel("字幕文件:")
self.subtitle_edit = QLineEdit()
self.subtitle_btn = QPushButton("选择")
self.subtitle_btn.clicked.connect(self.select_subtitle)
subtitle_layout.addWidget(self.subtitle_label)
subtitle_layout.addWidget(self.subtitle_edit)
subtitle_layout.addWidget(self.subtitle_btn)
file_layout.addLayout(subtitle_layout)
output_layout = QHBoxLayout()
self.output_label = QLabel("输出目录:")
self.output_edit = QLineEdit()
self.output_btn = QPushButton("选择")
self.output_btn.clicked.connect(self.select_output_dir)
output_layout.addWidget(self.output_label)
output_layout.addWidget(self.output_edit)
output_layout.addWidget(self.output_btn)
file_layout.addLayout(output_layout)
main_layout.addWidget(file_group)
control_layout = QHBoxLayout()
self.start_btn = QPushButton("开始剪辑")
self.start_btn.clicked.connect(self.start_clipping)
self.stop_btn = QPushButton("取消剪辑")
self.stop_btn.clicked.connect(self.stop_clipping)
self.stop_btn.setEnabled(False)
control_layout.addWidget(self.start_btn)
control_layout.addWidget(self.stop_btn)
main_layout.addLayout(control_layout)
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
main_layout.addWidget(self.progress_bar)
log_group = QGroupBox("操作日志")
log_layout = QVBoxLayout(log_group)
self.log_edit = QTextEdit()
self.log_edit.setReadOnly(True)
log_layout.addWidget(self.log_edit)
main_layout.addWidget(log_group, 1)
def bind_signals(self):
self.signals.progress.connect(self.update_progress)
self.signals.log.connect(self.update_log)
self.signals.error.connect(self.show_error)
self.signals.finished.connect(self.clipping_finished)
def select_video(self):
path, _ = QFileDialog.getOpenFileName(self, "选择视频文件", "", "视频文件 (*.mp4 *.avi *.mov *.mkv)")
if path:
self.video_edit.setText(path)
def select_subtitle(self):
path, _ = QFileDialog.getOpenFileName(self, "选择字幕文件", "", "字幕文件 (*.srt)")
if path:
self.subtitle_edit.setText(path)
def select_output_dir(self):
path = QFileDialog.getExistingDirectory(self, "选择输出目录")
if path:
self.output_edit.setText(path)
def parse_subtitle(self, path: str) -> List[SubtitleSegment]:
segments = []
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read().strip().split("\n\n")
for block in content:
lines = block.split("\n")
if len(lines) >= 3:
time_line = lines[1].strip().replace(",", ".")
start_time, end_time = time_line.split(" --> ")
text = "\n".join(lines[2:])
segments.append(SubtitleSegment(start_time, end_time, text))
self.signals.log.emit(f"成功解析 {len(segments)} 个字幕片段")
except Exception as e:
self.signals.error.emit(f"字幕解析失败:{str(e)}")
return segments
def start_clipping(self):
video_path = self.video_edit.text().strip()
subtitle_path = self.subtitle_edit.text().strip()
output_dir = self.output_edit.text().strip()
if not all([video_path, subtitle_path, output_dir]):
QMessageBox.warning(self, "警告", "请完善所有文件路径设置!")
return
if not os.path.exists(video_path):
QMessageBox.warning(self, "警告", "视频文件不存在!")
return
if not os.path.exists(subtitle_path):
QMessageBox.warning(self, "警告", "字幕文件不存在!")
return
segments = self.parse_subtitle(subtitle_path)
if not segments:
QMessageBox.warning(self, "警告", "未解析到有效字幕片段!")
return
self.progress_bar.setValue(0)
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.clip_task = ClipTask(video_path, output_dir, segments, self.signals)
QThreadPool.globalInstance().start(self.clip_task)
def stop_clipping(self):
if self.clip_task:
self.clip_task.stop()
self.signals.log.emit("正在取消剪辑任务...")
def update_progress(self, value):
self.progress_bar.setValue(value)
def update_log(self, msg):
self.log_edit.append(msg)
def show_error(self, msg):
QMessageBox.critical(self, "错误", msg)
self.clipping_finished()
def clipping_finished(self):
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.clip_task = None
if __name__ == "__main__":
app = QApplication(sys.argv)
window = VideoClipWindow()
window.show()
sys.exit(app.exec())