支持设置时间选区

支持方向键快进快退(ctrl + -> or shirft + -> or ->)
This commit is contained in:
Jason
2025-05-19 09:54:21 +08:00
parent 7cf4e882cd
commit e26e23ad6a
15 changed files with 488 additions and 91 deletions

View File

@@ -1,5 +1,5 @@
import os
from enum import Enum
from enum import Enum, unique
from dataclasses import dataclass
from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QAbstractItemView, QTableWidgetItem, QHeaderView
from PySide6.QtCore import Qt, Signal, QModelIndex, QUrl
@@ -9,12 +9,18 @@ from showinfm import show_in_file_manager
from backend.config import tr
@unique
class TaskStatus(Enum):
PENDING = tr['TaskList']['Pending']
PROCESSING = tr['TaskList']['Processing']
COMPLETED = tr['TaskList']['Completed']
FAILED = tr['TaskList']['Failed']
@unique
class TaskOptions(Enum):
AB_SECTIONS = "ab_sections"
@dataclass
class Task:
path: str
@@ -22,6 +28,7 @@ class Task:
progress: int
status: TaskStatus
output_path: str
options: dict
class TaskListComponent(QWidget):
"""任务列表组件"""
@@ -39,9 +46,9 @@ class TaskListComponent(QWidget):
self.current_task_index = -1 # 当前选中的任务索引
# 创建布局
self.__initWidget()
self.__init_widgets()
def __initWidget(self):
def __init_widgets(self):
"""初始化组件"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
@@ -95,6 +102,7 @@ class TaskListComponent(QWidget):
progress=0,
status=TaskStatus.PENDING,
output_path=output_path,
options={},
)
self.tasks.append(task)
@@ -296,6 +304,14 @@ class TaskListComponent(QWidget):
self.current_task_index = index
self.table.selectRow(index)
self.table.scrollTo(self.table.model().index(index, 0))
def get_current_task_index(self):
"""获取当前处理的任务索引
Returns:
int: 任务索引
"""
return self.current_task_index
def select_task(self, index):
"""选中指定任务
@@ -304,6 +320,8 @@ class TaskListComponent(QWidget):
index: 任务索引
"""
self.set_current_task(index)
if 0 <= index < len(self.tasks):
self.task_selected.emit(index, self.tasks[index].path)
def open_file_location(self, path):
"""打开文件所在位置
@@ -328,4 +346,27 @@ class TaskListComponent(QWidget):
parent = self
while parent.parent():
parent = parent.parent()
return parent
return parent
def update_task_option(self, index, task_option: TaskOptions, value):
"""更新任务选项
Args:
index: 任务索引
task_option: 选项名
value: 选项值
"""
if 0 <= index < len(self.tasks):
self.tasks[index].options[task_option.value] = value
def get_task_option(self, index, task_option: TaskOptions, default=None):
"""获取任务选项
Args:
index: 任务索引
task_option: 选项名
default: 默认值
Returns:
选项值
"""
if 0 <= index < len(self.tasks):
return self.tasks[index].options.get(task_option.value, default)

View File

@@ -1,6 +1,7 @@
import cv2
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy
from PySide6.QtCore import Qt, Signal, QRect, QRectF, QTimer, QObject, QEvent
from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu
from PySide6.QtCore import Qt, Signal, QRect, QRectF, QObject, QEvent
from PySide6.QtGui import QAction, QShortcut
from PySide6 import QtCore, QtWidgets, QtGui
from qfluentwidgets import qconfig, CardWidget, HollowHandleStyle
@@ -11,6 +12,7 @@ class VideoDisplayComponent(QWidget):
# 定义信号
selection_changed = Signal(list) # 选择框变化信号
ab_sections_changed = Signal(list) # AB分区变化信号
def __init__(self, parent=None):
super().__init__(parent)
@@ -27,8 +29,12 @@ class VideoDisplayComponent(QWidget):
self.edge_size = 10 # 调整大小的边缘区域
self.enable_mouse_events = True # 控制是否启用鼠标事件
# 安装事件过滤器以捕获键盘事件
self.installEventFilter(self)
# AB分区标记相关变量
self.ab_sections = [] # 存储AB分区标记 [range(start, end), ...]
self.current_ab_start = -1 # 当前AB分区的起点
# 创建右键菜单
self.__init_context_menu()
# 获取屏幕大小
screen = QtWidgets.QApplication.primaryScreen().size()
@@ -49,10 +55,12 @@ class VideoDisplayComponent(QWidget):
self.scaled_height = None
self.border_left = 0
self.border_top = 0
self.fps = 30
self.__initWidget()
self.__init_widgets()
self.__init_shotcuts()
def __initWidget(self):
def __init_widgets(self):
"""初始化组件"""
main_layout = QVBoxLayout(self)
main_layout.setSpacing(0)
@@ -156,6 +164,51 @@ class VideoDisplayComponent(QWidget):
self.video_container.setLayout(video_layout)
main_layout.addWidget(self.video_container)
def __init_shotcuts(self):
"""初始化快捷键"""
self.shortcut_ab_start = QShortcut(QtGui.QKeySequence("["), self)
self.shortcut_ab_start.activated.connect(self.__handle_mark_for_ab_start)
self.shortcut_ab_start.setContext(Qt.ApplicationShortcut)
self.shortcut_ab_end = QShortcut(QtGui.QKeySequence("]"), self)
self.shortcut_ab_end.activated.connect(self.__handle_mark_for_ab_end)
self.shortcut_ab_end.setContext(Qt.ApplicationShortcut)
self.shortcut_ab_delete = QShortcut(QtGui.QKeySequence("\\"), self)
self.shortcut_ab_delete.activated.connect(self.__handle_delete_ab_section)
self.shortcut_ab_delete.setContext(Qt.ApplicationShortcut)
self.shortcut_delete_selection = QShortcut(QtGui.QKeySequence.Delete, self)
self.shortcut_delete_selection.activated.connect(self.__handle_delete_selection)
self.shortcut_delete_selection.setContext(Qt.ApplicationShortcut)
# 添加左右键控制slider的快捷键
self.shortcut_right = QShortcut(QtGui.QKeySequence(Qt.Key_Right), self)
self.shortcut_right.activated.connect(lambda: self.__adjust_slider_value(self.fps))
self.shortcut_right.setContext(Qt.ApplicationShortcut)
self.shortcut_left = QShortcut(QtGui.QKeySequence(Qt.Key_Left), self)
self.shortcut_left.activated.connect(lambda: self.__adjust_slider_value(-self.fps))
self.shortcut_left.setContext(Qt.ApplicationShortcut)
# 添加Ctrl+左右键控制slider的快捷键
self.shortcut_ctrl_right = QShortcut(QtGui.QKeySequence("Ctrl+Right"), self)
self.shortcut_ctrl_right.activated.connect(lambda: self.__adjust_slider_value(self.fps*5))
self.shortcut_ctrl_right.setContext(Qt.ApplicationShortcut)
self.shortcut_ctrl_left = QShortcut(QtGui.QKeySequence("Ctrl+Left"), self)
self.shortcut_ctrl_left.activated.connect(lambda: self.__adjust_slider_value(-self.fps*5))
self.shortcut_ctrl_left.setContext(Qt.ApplicationShortcut)
# 添加Shift+左右键控制slider的快捷键
self.shortcut_shift_right = QShortcut(QtGui.QKeySequence("Shift+Right"), self)
self.shortcut_shift_right.activated.connect(lambda: self.__adjust_slider_value(1))
self.shortcut_shift_right.setContext(Qt.ApplicationShortcut)
self.shortcut_shift_left = QShortcut(QtGui.QKeySequence("Shift+Left"), self)
self.shortcut_shift_left.activated.connect(lambda: self.__adjust_slider_value(-1))
self.shortcut_shift_left.setContext(Qt.ApplicationShortcut)
def update_video_display(self, frame, draw_selection=True):
"""更新视频显示"""
if frame is None:
@@ -217,10 +270,10 @@ class VideoDisplayComponent(QWidget):
# 创建新的选择框
self.selection_rects.append(QRect(x, y, w, h))
# 更新视频显示
self.update_preview_with_rect()
# 更新视频显示
self.update_preview_with_rect(draw_selection=draw_selection)
def update_preview_with_rect(self, rect=None):
def update_preview_with_rect(self, rect=None, draw_selection=True):
"""更新带有选择框的预览"""
if not hasattr(self, 'current_pixmap') or self.current_pixmap is None:
return
@@ -234,26 +287,71 @@ class VideoDisplayComponent(QWidget):
painter = QtGui.QPainter(pixmap_copy)
# 绘制所有选区
for i, rect in enumerate(self.selection_rects):
# 设置选择框样式
if i == self.active_selection_index:
# 活动选区使用绿色
pen = QtGui.QPen(QtGui.QColor(0, 255, 0))
else:
# 非活动选区使用黄色
pen = QtGui.QPen(QtGui.QColor(255, 255, 0))
if draw_selection:
for i, rect in enumerate(self.selection_rects):
# 设置选择框样式
if i == self.active_selection_index:
# 活动选区使用绿色
pen = QtGui.QPen(QtGui.QColor(0, 255, 0))
else:
# 非活动选区使用黄色
pen = QtGui.QPen(QtGui.QColor(255, 255, 0))
pen.setWidth(2)
painter.setPen(pen)
# 绘制选择框
painter.drawRect(rect)
# 如果正在绘制新选区,也绘制它
if self.is_drawing and self.selection_rect.isValid():
pen = QtGui.QPen(QtGui.QColor(0, 255, 0)) # 绿色
pen.setWidth(2)
painter.setPen(pen)
painter.drawRect(self.selection_rect)
# 绘制AB分区标记
total_frames = self.video_slider.maximum()
if total_frames > 0 and self.ab_sections:
# 在视频显示区域下方5像素处绘制AB分区标记
ab_rect_height = 5
ab_rect_y = pixmap_copy.height() - ab_rect_height
# 设置半透明白色画刷
painter.setPen(Qt.NoPen)
painter.setBrush(QtGui.QColor(255, 255, 255, 128)) # 半透明白色
# 计算可用宽度(考虑左右边距)
left_margin = 15
right_margin = 15
available_width = pixmap_copy.width() - left_margin - right_margin
for section_range in self.ab_sections:
# 计算相对位置
start_x = left_margin + int((section_range.start / total_frames) * available_width)
end_x = left_margin + int((section_range.stop / total_frames) * available_width)
# 绘制AB分区矩形
painter.drawRect(start_x, ab_rect_y, end_x - start_x, ab_rect_height)
# 绘制current_ab_start的高亮竖线
if self.current_ab_start >= 0 and total_frames > 0:
# 计算可用宽度(考虑左右边距)
left_margin = 15
right_margin = 15
available_width = pixmap_copy.width() - left_margin - right_margin
# 计算current_ab_start的相对位置
start_x = left_margin + int((self.current_ab_start / total_frames) * available_width)
# 设置高亮白色画笔
pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) # 纯白色
pen.setWidth(2)
painter.setPen(pen)
# 绘制选择框
painter.drawRect(rect)
# 如果正在绘制新选区,也绘制它
if self.is_drawing and self.selection_rect.isValid():
pen = QtGui.QPen(QtGui.QColor(0, 255, 0)) # 绿色
pen.setWidth(2)
painter.setPen(pen)
painter.drawRect(self.selection_rect)
# 绘制高亮竖线高度为5像素
ab_line_height = 5
ab_line_y = pixmap_copy.height() - ab_line_height
painter.drawLine(start_x, ab_line_y, start_x, pixmap_copy.height())
painter.end()
@@ -264,8 +362,12 @@ class VideoDisplayComponent(QWidget):
"""鼠标按下事件处理"""
if not self.enable_mouse_events:
return
# 设置焦点到当前组件
self.setFocus()
# 处理右键点击,显示上下文菜单
if event.button() == Qt.RightButton:
self.context_menu.exec_(event.globalPos())
return
pos = event.pos()
# 检查是否按下了Ctrl键
@@ -559,7 +661,10 @@ class VideoDisplayComponent(QWidget):
# 如果鼠标不在任何选区上,设置为默认光标
self.video_display.setCursor(Qt.ArrowCursor)
def set_video_parameters(self, frame_width, frame_height, scaled_width=None, scaled_height=None, border_left=0, border_top=0):
def set_video_parameters(self, frame_width, frame_height,
scaled_width=None, scaled_height=None,
border_left=0, border_top=0,
fps=30):
"""设置视频参数"""
self.frame_width = frame_width
self.frame_height = frame_height
@@ -567,6 +672,7 @@ class VideoDisplayComponent(QWidget):
self.scaled_height = scaled_height
self.border_left = border_left
self.border_top = border_top
self.fps = fps
def get_selection_coordinates(self):
"""获取选择框坐标"""
@@ -739,27 +845,175 @@ class VideoDisplayComponent(QWidget):
self.update_preview_with_rect()
self.selection_changed.emit(self.selection_rects)
def eventFilter(self, obj, event):
"""事件过滤器,用于处理键盘事件"""
if event.type() == QEvent.KeyPress:
# 处理退格键或删除键
if (event.key() == Qt.Key_Backspace or event.key() == Qt.Key_Delete) and self.active_selection_index >= 0:
# 删除当前活跃选区
self.selection_rects.pop(self.active_selection_index)
if self.selection_ratios:
self.selection_ratios.pop(self.active_selection_index)
# 如果还有选区,将最后一个选区设为活跃选区
if self.selection_rects:
self.active_selection_index = len(self.selection_rects) - 1
else:
self.active_selection_index = -1
self.save_selections_to_configs()
# 更新显示并发送选区变化信号
self.update_preview_with_rect()
self.selection_changed.emit(self.selection_rects)
return True
def __handle_delete_selection(self):
"""处理删除当前选区的逻辑"""
if self.active_selection_index >= 0 and self.selection_rects:
# 删除当前活跃选区
self.selection_rects.pop(self.active_selection_index)
if self.selection_ratios:
self.selection_ratios.pop(self.active_selection_index)
# 如果还有其他选区,将最后一个选区设为活跃选区
if self.selection_rects:
self.active_selection_index = len(self.selection_rects) - 1
else:
self.active_selection_index = -1
self.save_selections_to_configs()
# 更新显示
self.update_preview_with_rect()
# 发送选区变化信号
self.selection_changed.emit(self.selection_rects)
return True
def __handle_mark_for_ab_start(self):
"""处理标记AB分区起点的逻辑"""
current_frame = self.video_slider.value()
if current_frame >= 0:
# 检查是否需要调整已有区间
adjusted = False
for i, section_range in enumerate(self.ab_sections):
if current_frame in section_range:
# 调整已有区间的起点
self.ab_sections[i] = range(current_frame, section_range.stop)
adjusted = True
break
if not adjusted:
# 记录新的AB分区起点
self.current_ab_start = current_frame
# 更新显示
self.update_preview_with_rect()
return True
return False
def __handle_mark_for_ab_end(self):
"""处理标记AB分区终点的逻辑"""
current_frame = self.video_slider.value()
if current_frame >= 0 and self.current_ab_start >= 0:
# 检查是否需要调整已有区间
adjusted = False
for i, section_range in enumerate(self.ab_sections):
if current_frame in section_range:
# 调整已有区间的终点
self.ab_sections[i] = range(section_range.start, current_frame + 1)
adjusted = True
break
if not adjusted and self.current_ab_start != current_frame:
# 添加新的AB分区
self.ab_sections.append(range(self.current_ab_start, current_frame + 1))
self.current_ab_start = -1 # 重置起点
self.ab_sections_changed.emit(self.ab_sections)
# 更新显示
self.update_preview_with_rect()
return True
return False
def __handle_delete_ab_section(self):
"""处理删除当前AB区块的逻辑"""
current_frame = self.video_slider.value()
if current_frame >= 0 and self.ab_sections:
# 查找当前帧所在的AB区块
for i, section_range in enumerate(self.ab_sections):
if current_frame in section_range:
# 删除该AB区块
self.ab_sections.pop(i)
# 如果当前有标记的起点,且在被删除的区块内,重置起点
if self.current_ab_start in section_range:
self.current_ab_start = -1
# 发送AB区块变化信号
self.ab_sections_changed.emit(self.ab_sections)
# 更新显示
self.update_preview_with_rect()
return True
return False
def __adjust_slider_value(self, delta):
"""调整视频滑块的值"""
current_value = self.video_slider.value()
max_value = self.video_slider.maximum()
new_value = current_value + int(delta)
# 确保新值在有效范围内
if new_value < self.video_slider.minimum():
new_value = self.video_slider.minimum()
elif new_value > max_value:
new_value = max_value
# 设置新值
self.video_slider.setValue(new_value)
def eventFilter(self, obj, event):
"""事件过滤器,处理键盘事件"""
if event.type() == QEvent.KeyPress:
# 处理退格键和删除键
if event.key() == Qt.Key_Backspace or event.key() == Qt.Key_Delete:
if self.__handle_delete_selection():
return True
# 对于其他事件,继续传递给父类处理
return super().eventFilter(obj, event)
return super().eventFilter(obj, event)
def __init_context_menu(self):
"""初始化右键菜单"""
self.context_menu = QMenu(self)
# 设定区块起点动作
self.action_mark_ab_start = QAction(tr['SubtitleExtractorGUI']['MarkABStart'], self)
self.action_mark_ab_start.setShortcut("[")
self.action_mark_ab_start.triggered.connect(self.__handle_mark_for_ab_start)
self.context_menu.addAction(self.action_mark_ab_start)
# 设定区块终点动作
self.action_mark_ab_end = QAction(tr['SubtitleExtractorGUI']['MarkABEnd'], self)
self.action_mark_ab_end.setShortcut("]")
self.action_mark_ab_end.triggered.connect(self.__handle_mark_for_ab_end)
self.context_menu.addAction(self.action_mark_ab_end)
self.action_mark_ab_delete = QAction(tr['SubtitleExtractorGUI']['DeleteABSection'], self)
self.action_mark_ab_delete.setShortcut("\\")
self.action_mark_ab_delete.triggered.connect(self.__handle_delete_ab_section)
self.context_menu.addAction(self.action_mark_ab_delete)
self.action_delete_selection = QAction(tr['SubtitleExtractorGUI']['DeleteSelection'], self)
self.action_delete_selection.setShortcut("DELETE")
self.action_delete_selection.triggered.connect(self.__handle_delete_selection)
self.context_menu.addAction(self.action_delete_selection)
def get_ab_sections(self):
"""获取AB分区标记"""
return self.ab_sections
def set_ab_sections(self, sections):
"""设置AB分区标记"""
self.ab_sections = sections
self.update_preview_with_rect()
def clear_ab_sections(self):
"""清除所有AB分区标记"""
self.ab_sections = []
self.current_ab_start = -1
self.update_preview_with_rect()
def closeEvent(self, event):
"""窗口关闭时断开信号连接"""
try:
# 断开信号连接
self.shortcut_ab_start.activated.disconnect(self.__handle_mark_for_ab_start)
self.shortcut_ab_end.activated.disconnect(self.__handle_mark_for_ab_end)
self.shortcut_ab_delete.activated.disconnect(self.__handle_delete_ab_section)
self.action_mark_ab_start.triggered.disconnect(self.__handle_mark_for_ab_start)
self.action_mark_ab_end.triggered.disconnect(self.__handle_mark_for_ab_end)
self.action_mark_ab_delete.triggered.disconnect(self.__handle_delete_ab_section)
self.action_delete_selection.triggered.disconnect(self.__handle_delete_selection)
self.shortcut_delete_selection.activated.disconnect(self.__handle_delete_selection)
except Exception as e:
print(f"Error during close window:", e)
super().closeEvent(event)