From e26e23ad6a4ea9ead66d238483902b2eb9793c5c Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 19 May 2025 09:54:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=AE=BE=E7=BD=AE=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E9=80=89=E5=8C=BA=20=E6=94=AF=E6=8C=81=E6=96=B9?= =?UTF-8?q?=E5=90=91=E9=94=AE=E5=BF=AB=E8=BF=9B=E5=BF=AB=E9=80=80(ctrl=20+?= =?UTF-8?q?=20->=20or=20shirft=20+=20->=20or=20->)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/inpaint/sttn_auto_inpaint.py | 44 ++- backend/interface/ch.ini | 8 +- backend/interface/chinese_cht.ini | 8 +- backend/interface/en.ini | 8 +- backend/interface/es.ini | 8 +- backend/interface/japan.ini | 8 +- backend/interface/ko.ini | 8 +- backend/interface/vi.ini | 8 +- backend/main.py | 4 + backend/tools/inpaint_tools.py | 20 ++ backend/tools/subtitle_detect.py | 4 + ui/advanced_setting_interface.py | 4 +- ui/component/task_list_component.py | 49 +++- ui/component/video_display_component.py | 356 ++++++++++++++++++++---- ui/home_interface.py | 42 ++- 15 files changed, 488 insertions(+), 91 deletions(-) diff --git a/backend/inpaint/sttn_auto_inpaint.py b/backend/inpaint/sttn_auto_inpaint.py index f6f7b05..321d5a9 100644 --- a/backend/inpaint/sttn_auto_inpaint.py +++ b/backend/inpaint/sttn_auto_inpaint.py @@ -15,7 +15,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspa from backend.config import config from backend.inpaint.sttn.auto_sttn import InpaintGenerator from backend.inpaint.utils.sttn_utils import Stack, ToTorchFormatTensor -from backend.tools.inpaint_tools import get_inpaint_area_by_mask +from backend.tools.inpaint_tools import get_inpaint_area_by_mask, is_frame_number_in_ab_sections # 定义图像预处理方式 _to_tensors = transforms.Compose([ @@ -205,8 +205,11 @@ class STTNAutoInpaint: # 读取视频帧信息 reader, frame_info = self.read_frame_info_from_video() if input_sub_remover is not None: + ab_sections = input_sub_remover.ab_sections + writer = input_sub_remover.video_writer else: + ab_sections = None # 创建视频写入对象,用于输出修复后的视频 writer = cv2.VideoWriter(self.video_out_path, cv2.VideoWriter_fourcc(*"mp4v"), frame_info['fps'], (frame_info['W_ori'], frame_info['H_ori'])) @@ -249,11 +252,12 @@ class STTNAutoInpaint: frames_hr.append(image) valid_frames_count += 1 - for k in range(len(inpaint_area)): - # 裁剪、缩放并添加到帧字典 - image_crop = image[inpaint_area[k][0]:inpaint_area[k][1], :, :] - image_resize = cv2.resize(image_crop, (self.sttn_inpaint.model_input_width, self.sttn_inpaint.model_input_height)) - frames[k].append(image_resize) + if is_frame_number_in_ab_sections(j, ab_sections): + for k in range(len(inpaint_area)): + # 裁剪、缩放并添加到帧字典 + image_crop = image[inpaint_area[k][0]:inpaint_area[k][1], :, :] + image_resize = cv2.resize(image_crop, (self.sttn_inpaint.model_input_width, self.sttn_inpaint.model_input_height)) + frames[k].append(image_resize) # 如果没有读取到有效帧,则跳过当前迭代 if valid_frames_count == 0: @@ -269,6 +273,17 @@ class STTNAutoInpaint: # 如果有要修复的区域 if inpaint_area and valid_frames_count > 0: + # 创建一个映射,记录哪些帧被处理了以及它们在frames[k]中的索引 + processed_frames_map = {} + processed_idx = 0 + + # 构建映射关系 + for j in range(start_f, end_f): + if j - start_f < valid_frames_count and is_frame_number_in_ab_sections(j, ab_sections): + processed_frames_map[j - start_f] = processed_idx + processed_idx += 1 + + # 应用修复结果 for j in range(valid_frames_count): if input_sub_remover is not None and input_sub_remover.gui_mode: original_frame = copy.deepcopy(frames_hr[j]) @@ -277,13 +292,16 @@ class STTNAutoInpaint: frame = frames_hr[j] - for k in range(len(inpaint_area)): - if j < len(comps[k]): # 确保索引有效 - # 将修复的图像重新扩展到原始分辨率,并融合到原始帧 - comp = cv2.resize(comps[k][j], (frame_info['W_ori'], split_h)) - comp = cv2.cvtColor(np.array(comp).astype(np.uint8), cv2.COLOR_BGR2RGB) - mask_area = mask[inpaint_area[k][0]:inpaint_area[k][1], :] - frame[inpaint_area[k][0]:inpaint_area[k][1], :, :] = mask_area * comp + (1 - mask_area) * frame[inpaint_area[k][0]:inpaint_area[k][1], :, :] + # 只有被处理过的帧才应用修复结果 + if j in processed_frames_map: + comp_idx = processed_frames_map[j] + for k in range(len(inpaint_area)): + if comp_idx < len(comps[k]): # 确保索引有效 + # 将修复的图像重新扩展到原始分辨率,并融合到原始帧 + comp = cv2.resize(comps[k][comp_idx], (frame_info['W_ori'], split_h)) + comp = cv2.cvtColor(np.array(comp).astype(np.uint8), cv2.COLOR_BGR2RGB) + mask_area = mask[inpaint_area[k][0]:inpaint_area[k][1], :] + frame[inpaint_area[k][0]:inpaint_area[k][1], :, :] = mask_area * comp + (1 - mask_area) * frame[inpaint_area[k][0]:inpaint_area[k][1], :, :] writer.write(frame) diff --git a/backend/interface/ch.ini b/backend/interface/ch.ini index c0035f8..d2c3e2c 100644 --- a/backend/interface/ch.ini +++ b/backend/interface/ch.ini @@ -56,7 +56,6 @@ Setting = 设置 OpenVideoSuccess = 成功打开视频 OpenVideoFailed = 无法打开视频: {}, 格式不兼容或文件损坏 OpenVideoFirst = 请先打开视频 -SubtitleArea = 字幕区域 VideoPreview = 视频预览 InterfaceLanguage = 界面语言 InpaintMode = 处理模型 @@ -68,6 +67,10 @@ InpaintModeDesc = STTN智能擦除, 对于真人视频效果较好,速度快, OpenCV: 极速模式, 不保证inpaint效果,仅仅对包含文本的区域文本进行去除(显存要求较低) SubtitleDetectMode = 字幕检测 ErrorDuringProcessing = 处理过程中发生错误: {} +MarkABStart = 设定处理区块起点 +MarkABEnd = 设定处理区块终点 +DeleteABSection = 删除当前处理区块 +DeleteSelection = 删除当前激活选区 [Main] SubtitleDetectionAcceleratorON = 字幕检测使用{}进行加速 @@ -87,6 +90,9 @@ ProcessingTime = 处理时间: {}秒 FailToMergeAudio = 合并音频失败: {} FailToExtractAudio = 提取音频失败: {} CopyFileFailed = 复制文件 {} 到 {} 失败, 原因: {} +SubtitleArea = 字幕区域: {} +ABSection = 处理区块: {} +ABSectionAll = 全部 [TaskList] Pending = 待处理 diff --git a/backend/interface/chinese_cht.ini b/backend/interface/chinese_cht.ini index 08f23ba..51663d7 100644 --- a/backend/interface/chinese_cht.ini +++ b/backend/interface/chinese_cht.ini @@ -56,7 +56,6 @@ Setting = 設定 OpenVideoSuccess = 成功開啟影片 OpenVideoFailed = 無法開啟影片: {},格式不相容或檔案損毀 OpenVideoFirst = 請先開啟影片 -SubtitleArea = 字幕區域 VideoPreview = 影片預覽 InterfaceLanguage = 介面語言 InpaintMode = 處理模型 @@ -68,6 +67,10 @@ InpaintModeDesc = STTN智能擦除,對於真人視頻效果較好,速度快 OpenCV:極速模式,不保證inpaint效果,僅僅對包含文本的區域文本進行去除(顯存要求較低) SubtitleDetectMode = 字幕檢測模式 ErrorDuringProcessing = 處理過程中發生錯誤: {} +MarkABStart = 設定處理區塊起點 +MarkABEnd = 設定處理區塊終點 +DeleteABSection = 刪除當前處理區塊 +DeleteSelection = 刪除當前激活選區 [Main] SubtitleDetectionAcceleratorON = 字幕檢測使用{}進行加速 @@ -87,6 +90,9 @@ ProcessingTime = 處理時間: {}秒 FailToMergeAudio = 合併音訊失敗: {} FailToExtractAudio = 提取音訊失敗: {} CopyFileFailed = 複製檔案 {} 至 {} 失敗,原因: {} +SubtitleArea = 字幕區域: {} +ABSection = 處理區塊: {} +ABSectionAll = 全部 [TaskList] Pending = 待處理 diff --git a/backend/interface/en.ini b/backend/interface/en.ini index 417e274..8620126 100644 --- a/backend/interface/en.ini +++ b/backend/interface/en.ini @@ -56,7 +56,6 @@ Setting = Settings OpenVideoSuccess = Video opened successfully OpenVideoFailed = Failed to open video: {} (invalid format or corrupted file) OpenVideoFirst = Please open a video first -SubtitleArea = Subtitle Area VideoPreview = Video Preview InterfaceLanguage = Interface Language InpaintMode = Processing Model @@ -68,6 +67,10 @@ InpaintModeDesc = STTN Smart Inpainting: Best for real-person videos, fast speed OpenCV: Ultra-fast mode, inpainting effect not guaranteed, only removes text in detected regions (low VRAM requirement) SubtitleDetectMode = Subtitle Detection ErrorDuringProcessing = Error during processing: {} +MarkABStart = Mark Start +MarkABEnd = Mark End +DeleteABSection = Delete Section +DeleteSelection = Delete Selection [Main] SubtitleDetectionAcceleratorON = Subtitle detection accelerated with {} @@ -87,6 +90,9 @@ ProcessingTime = Processing time: {} seconds FailToMergeAudio = Audio merge failed: {} FailToExtractAudio = Audio extraction failed: {} CopyFileFailed = Failed to copy {} to {}. Reason: {} +SubtitleArea = Subtitle Area: {} +ABSection = Processing block: {} +ABSectionAll = All [TaskList] Pending = Pending diff --git a/backend/interface/es.ini b/backend/interface/es.ini index e38666e..6953b00 100644 --- a/backend/interface/es.ini +++ b/backend/interface/es.ini @@ -56,7 +56,6 @@ Setting = Configuración OpenVideoSuccess = Video abierto correctamente OpenVideoFailed = Error al abrir video: {} (formato incompatible o archivo dañado) OpenVideoFirst = Abre un video primero -SubtitleArea = Área de subtítulos VideoPreview = Vista previa InterfaceLanguage = Idioma de interfaz InpaintMode = Modelo de procesamiento @@ -68,6 +67,10 @@ InpaintModeDesc = STTN Borrado inteligente: Mejor para videos de personas reales OpenCV: Modo ultra rápido, el efecto de borrado no está garantizado, solo elimina texto en las áreas detectadas (bajo requerimiento de VRAM) SubtitleDetectMode = Detección de subtítulos ErrorDuringProcessing = Error durante el procesamiento: {} +MarkABStart = Marcar inicio +MarkABEnd = Marcar fin +DeleteABSection = Eliminar sección +DeleteSelection = Eliminar selección [Main] SubtitleDetectionAcceleratorON = Detección de subtítulos acelerada con {} @@ -87,6 +90,9 @@ ProcessingTime = Tiempo procesamiento: {} segundos FailToMergeAudio = Error mezclando audio: {} FailToExtractAudio = Error extrayendo audio: {} CopyFileFailed = Error copiando {} a {}. Razón: {} +SubtitleArea = Área de subtítulos: {} +ABSection = Procesando bloque: {} +ABSectionAll = Todo [TaskList] Pending = Pendiente diff --git a/backend/interface/japan.ini b/backend/interface/japan.ini index aa0cd16..ef39399 100644 --- a/backend/interface/japan.ini +++ b/backend/interface/japan.ini @@ -56,7 +56,6 @@ Setting = 設定 OpenVideoSuccess = 動画を正常に開きました OpenVideoFailed = 動画を開けません: {}(形式非対応/ファイル破損) OpenVideoFirst = 動画を先に開いてください -SubtitleArea = 字幕領域 VideoPreview = 動画プレビュー InterfaceLanguage = インターフェース言語 InpaintMode = 処理モデル @@ -68,6 +67,10 @@ InpaintModeDesc = STTNスマート消去:実写動画に最適、高速、ス OpenCV:超高速モード、消去効果は保証されません、検出されたテキスト領域のみ削除(VRAM要件低め) SubtitleDetectMode = 字幕検出 ErrorDuringProcessing = 処理中にエラーが発生しました: {} +MarkABStart = 処理区間の開始を設定 +MarkABEnd = 処理区間の終了を設定 +DeleteABSection = 現在の処理区間を削除 +DeleteSelection = 現在のアクティブ選択範囲を削除 [Main] SubtitleDetectionAcceleratorON = 字幕検出を{}で加速 @@ -87,6 +90,9 @@ ProcessingTime = 処理時間: {}秒 FailToMergeAudio = 音声統合失敗: {} FailToExtractAudio = 音声抽出失敗: {} CopyFileFailed = ファイルコピー失敗 {} → {}。理由: {} +SubtitleArea = 字幕領域: {} +ABSection = 処理ブロック: {} +ABSectionAll = すべて [TaskList] Pending = 待機中 diff --git a/backend/interface/ko.ini b/backend/interface/ko.ini index abef289..eb1b8c2 100644 --- a/backend/interface/ko.ini +++ b/backend/interface/ko.ini @@ -56,7 +56,6 @@ Setting = 설정 OpenVideoSuccess = 동영상 열기 성공 OpenVideoFailed = 동영상 열기 실패: {} (형식 불일치/파일 손상) OpenVideoFirst = 동영상을 먼저 열어주세요 -SubtitleArea = 자막 영역 VideoPreview = 동영상 미리보기 InterfaceLanguage = 인터페이스 언어 InpaintMode = 처리 모델 @@ -68,6 +67,10 @@ InpaintModeDesc = STTN 스마트 지우기: 실제 인물 영상에 적합, 빠 OpenCV: 초고속 모드, 인페인트 효과 보장 안 됨, 텍스트 영역만 제거(VRAM 요구량 낮음) SubtitleDetectMode = 자막 감지 ErrorDuringProcessing = 처리 중 오류: {} +MarkABStart = 처리 구간 시작점 설정 +MarkABEnd = 처리 구간 종료점 설정 +DeleteABSection = 현재 처리 구간 삭제 +DeleteSelection = 현재 활성 선택 영역 삭제 [Main] SubtitleDetectionAcceleratorON = 자막 감지 {} 가속 사용 @@ -87,6 +90,9 @@ ProcessingTime = 처리 시간: {}초 FailToMergeAudio = 오디오 병합 실패: {} FailToExtractAudio = 오디오 추출 실패: {} CopyFileFailed = 파일 복사 실패 {} → {}. 이유: {} +SubtitleArea = 자막 영역: {} +ABSection = 처리 블록: {} +ABSectionAll = 전체 [TaskList] Pending = 대기중 diff --git a/backend/interface/vi.ini b/backend/interface/vi.ini index 894aeed..5caef78 100644 --- a/backend/interface/vi.ini +++ b/backend/interface/vi.ini @@ -56,7 +56,6 @@ Setting = Cài đặt OpenVideoSuccess = Mở video thành công OpenVideoFailed = Lỗi mở video: {} (định dạng không hỗ trợ) OpenVideoFirst = Vui lòng mở video trước -SubtitleArea = Vùng phụ đề VideoPreview = Xem trước video InterfaceLanguage = Ngôn ngữ giao diện InpaintMode = Chế độ xử lý @@ -68,6 +67,10 @@ InpaintModeDesc = STTN Xóa thông minh: Phù hợp cho video người thật, t OpenCV: Chế độ siêu nhanh, không đảm bảo hiệu quả xóa, chỉ xóa vùng chứa văn bản (yêu cầu VRAM thấp) SubtitleDetectMode = Chế độ phát hiện ErrorDuringProcessing = Lỗi khi xử lý: {} +MarkABStart = Đặt điểm bắt đầu vùng xử lý +MarkABEnd = Đặt điểm kết thúc vùng xử lý +DeleteABSection = Xóa vùng xử lý hiện tại +DeleteSelection = Xóa vùng chọn hiện tại [Main] SubtitleDetectionAcceleratorON = Phát hiện phụ đề được tăng tốc bằng {} @@ -87,6 +90,9 @@ ProcessingTime = Thời gian xử lý: {} giây FailToMergeAudio = Lỗi ghép audio: {} FailToExtractAudio = Lỗi trích xuất audio: {} CopyFileFailed = Lỗi sao chép {} → {}, lý do: {} +SubtitleArea = Vùng phụ đề: {} +ABSection = Xử lý khối: {} +ABSectionAll = Tất cả [TaskList] Pending = Đang chờ diff --git a/backend/main.py b/backend/main.py index 26340a4..54bafb8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -84,6 +84,8 @@ class SubtitleRemover: self.is_successful_merged = False # 进度监听器列表 self.progress_listeners = [] + # inpaint的frame_no区域列表, 默认为inpaint所有帧 + self.ab_sections = None @staticmethod def is_current_frame_no_start(frame_no, continuous_frame_no_list): @@ -180,6 +182,7 @@ class SubtitleRemover: self.video_writer.write(frame) # self.append_output(f'write frame: {index}') self.update_progress(tbar, increment=1) + self.update_preview_with_comp(frame, frame) continue # 如果有水印,判断该帧是不是开头帧 else: @@ -328,6 +331,7 @@ class SubtitleRemover: def run(self): # 记录开始时间 start_time = time.time() + self.append_output(tr['Main']['ABSection'].format(str(self.ab_sections).replace("range", "") if self.ab_sections is not None and len(self.ab_sections) > 0 else tr['Main']['ABSectionAll'])) # 如果使用GPU加速,则打印GPU加速提示 if self.hardware_accelerator.has_accelerator(): accelerator_name = self.hardware_accelerator.accelerator_name diff --git a/backend/tools/inpaint_tools.py b/backend/tools/inpaint_tools.py index 27116a5..fa2a470 100644 --- a/backend/tools/inpaint_tools.py +++ b/backend/tools/inpaint_tools.py @@ -300,5 +300,25 @@ def expand_frame_ranges(frame_ranges, backward_frame_count, forward_frame_count) return expanded_ranges +def is_frame_number_in_ab_sections(frame_no, ab_sections): + """ + 检查给定的帧号是否在指定的A/B区间内。 + + Args: + frame_no: 要检查的帧号 + ab_sections: 包含A/B区间的列表,格式为[range(start, end), ...] + + Returns: + 如果帧号在A/B区间内,返回True;否则返回False。 + """ + if ab_sections is None: + return True + if len(ab_sections) <= 0: + return True + for section in ab_sections: + if frame_no in section: + return True + return False + if __name__ == '__main__': multiprocessing.set_start_method("spawn") diff --git a/backend/tools/subtitle_detect.py b/backend/tools/subtitle_detect.py index 4e91a3b..0e98b5c 100644 --- a/backend/tools/subtitle_detect.py +++ b/backend/tools/subtitle_detect.py @@ -12,6 +12,7 @@ from .ocr import get_coordinates from backend.config import config, tr from backend.scenedetect import scene_detect from backend.scenedetect.detectors import ContentDetector +from backend.tools.inpaint_tools import is_frame_number_in_ab_sections class SubtitleDetect: """ @@ -73,6 +74,9 @@ class SubtitleDetect: break # 读取视频帧成功 current_frame_no += 1 + if not is_frame_number_in_ab_sections(current_frame_no - 1, sub_remover.ab_sections): + tbar.update(1) + continue temp_list = self.detect_subtitle(frame) if len(temp_list) > 0: subtitle_frame_no_box_dict[current_frame_no] = temp_list diff --git a/ui/advanced_setting_interface.py b/ui/advanced_setting_interface.py index a9d6364..135eb68 100644 --- a/ui/advanced_setting_interface.py +++ b/ui/advanced_setting_interface.py @@ -19,9 +19,9 @@ class AdvancedSettingInterface(ScrollArea): super().__init__(parent) self.parent = parent self.version_manager = VersionService() - self.__initWidget() + self.__init_widgets() - def __initWidget(self): + def __init_widgets(self): # 创建滚动内容的容器 self.scrollWidget = QtWidgets.QWidget(self) self.expandLayout = ExpandLayout(self.scrollWidget) diff --git a/ui/component/task_list_component.py b/ui/component/task_list_component.py index fde4bc8..e092aa2 100644 --- a/ui/component/task_list_component.py +++ b/ui/component/task_list_component.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/ui/component/video_display_component.py b/ui/component/video_display_component.py index f470a01..5cd876c 100644 --- a/ui/component/video_display_component.py +++ b/ui/component/video_display_component.py @@ -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) \ No newline at end of file + 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) \ No newline at end of file diff --git a/ui/home_interface.py b/ui/home_interface.py index 1cc76ed..4837b38 100644 --- a/ui/home_interface.py +++ b/ui/home_interface.py @@ -14,7 +14,7 @@ from qfluentwidgets import (qconfig, PushButton, CardWidget, SubtitleLabel, Plai FluentIcon, HollowHandleStyle) from ui.setting_interface import SettingInterface from ui.component.video_display_component import VideoDisplayComponent -from ui.component.task_list_component import TaskListComponent, TaskStatus +from ui.component.task_list_component import TaskListComponent, TaskStatus, TaskOptions from ui.icon.my_fluent_icon import MyFluentIcon from backend.config import config, tr from backend.tools.subtitle_remover_remote_call import SubtitleRemoverRemoteCall @@ -52,13 +52,13 @@ class HomeInterface(QWidget): # 当前正在处理的任务索引 self.current_processing_task_index = -1 - self.__initWidget() + self.__init_widgets() self.progress_signal.connect(self.update_progress) self.append_log_signal.connect(self.append_log) self.update_preview_with_comp_signal.connect(self.update_preview_with_comp) self.task_error_signal.connect(self.on_task_error) - def __initWidget(self): + def __init_widgets(self): """创建主页面""" main_layout = QHBoxLayout(self) main_layout.setSpacing(8) @@ -70,6 +70,7 @@ class HomeInterface(QWidget): # 创建视频显示组件 self.video_display_component = VideoDisplayComponent(self) + self.video_display_component.ab_sections_changed.connect(self.ab_sections_changed) left_layout.addWidget(self.video_display_component) # 获取视频显示和滑块的引用 @@ -162,7 +163,13 @@ class HomeInterface(QWidget): if ret: # 更新预览图像 self.update_preview(frame) - + + def ab_sections_changed(self, ab_sections): + get_current_task_index = self.task_list_component.get_current_task_index() + if get_current_task_index == -1: + return + self.task_list_component.update_task_option(get_current_task_index, TaskOptions.AB_SECTIONS, ab_sections) + def on_task_selected(self, index, file_path): """处理任务被选中事件 @@ -172,6 +179,8 @@ class HomeInterface(QWidget): """ # 加载选中的视频进行预览 self.load_video(file_path) + ab_sections = self.task_list_component.get_task_option(index, TaskOptions.AB_SECTIONS, []) + self.video_display_component.set_ab_sections(ab_sections) def on_task_deleted(self, index): """处理任务被删除事件 @@ -186,7 +195,6 @@ class HomeInterface(QWidget): task = self.task_list_component.get_task(0) if task: # 如果还有任务,选中第一个 - self.load_video(task.path) self.task_list_component.select_task(0) def update_preview(self, frame): @@ -199,7 +207,8 @@ class HomeInterface(QWidget): self.scaled_width if hasattr(self, 'scaled_width') else None, self.scaled_height if hasattr(self, 'scaled_height') else None, self.border_left if hasattr(self, 'border_left') else 0, - self.border_top if hasattr(self, 'border_top') else 0 + self.border_top if hasattr(self, 'border_top') else 0, + self.fps if self.fps is not None else 30, ) # 更新视频显示(这会同时保存current_pixmap) @@ -310,7 +319,6 @@ class HomeInterface(QWidget): self.video_cap.release() self.video_cap = None - # 获取字幕区域坐标(直接从视频显示组件获取) subtitle_areas = self.video_display_component.get_original_coordinates() if not subtitle_areas: @@ -319,7 +327,7 @@ class HomeInterface(QWidget): self.append_output(f"{tr['SubtitleExtractorGUI']['SubtitleArea']}: {subtitle_areas}") self.task_list_component.update_task_status(self.current_processing_task_index, TaskStatus.PROCESSING) - process = self.run_subtitle_remover_process(task.path, task.output_path, subtitle_areas) + process = self.run_subtitle_remover_process(task.path, task.output_path, subtitle_areas, task.options) # 更新任务状态为已完成 task = self.task_list_component.get_task(self.current_processing_task_index) @@ -355,7 +363,7 @@ class HomeInterface(QWidget): self.stop_button.setVisible(False) @staticmethod - def remover_process(queue, video_path, output_path, subtitle_area): + def remover_process(queue, video_path, output_path, subtitle_area, options): """ 在子进程中执行字幕提取的函数 @@ -363,12 +371,15 @@ class HomeInterface(QWidget): video_path: 视频文件路径 output_path: 输出文件路径 subtitle_area: 字幕区域坐标 (ymin, ymax, xmin, xmax) + options: 选项 """ sr = None try: from backend.main import SubtitleRemover sr = SubtitleRemover(video_path, subtitle_area, True) sr.video_out_path = output_path + for key in options: + setattr(sr, key, options[key]) sr.add_progress_listener(lambda progress, isFinished: SubtitleRemoverRemoteCall.remote_call_update_progress(queue, progress, isFinished)) sr.append_output = lambda *args: SubtitleRemoverRemoteCall.remote_call_append_log(queue, args) sr.manage_process = lambda pid: SubtitleRemoverRemoteCall.remote_call_manage_process(queue, pid) @@ -385,7 +396,7 @@ class HomeInterface(QWidget): # 修改run_subtitle_remover_process方法 - def run_subtitle_remover_process(self, video_path, output_path, subtitle_areas): + def run_subtitle_remover_process(self, video_path, output_path, subtitle_areas, options): """ 使用多进程执行字幕提取,并等待进程完成 @@ -393,6 +404,7 @@ class HomeInterface(QWidget): video_path: 视频文件路径 output_path: 输出文件路径 subtitle_areas: 字幕区域坐标 [(ymin, ymax, xmin, xmax)] + options: 任务选项 """ subtitle_remover_remote_caller = SubtitleRemoverRemoteCall() subtitle_remover_remote_caller.register_update_progress_callback(self.progress_signal.emit) @@ -401,7 +413,7 @@ class HomeInterface(QWidget): subtitle_remover_remote_caller.register_error_callback(self.task_error_signal.emit) process = multiprocessing.Process( target=HomeInterface.remover_process, - args=(subtitle_remover_remote_caller.queue, video_path, output_path, subtitle_areas) + args=(subtitle_remover_remote_caller.queue, video_path, output_path, subtitle_areas, options) ) try: if not self.running_task: @@ -563,7 +575,8 @@ class HomeInterface(QWidget): else: output_path = os.path.abspath(os.path.join(os.path.dirname(path), f'{Path(path).stem}_no_sub.mp4')) self.task_list_component.add_task(path, output_path) - self.task_list_component.select_task(max(0, self.task_list_component.find_task_index_by_path(path))) + index = max(0, self.task_list_component.find_task_index_by_path(path)) + self.task_list_component.select_task(index) def closeEvent(self, event): """窗口关闭时断开信号连接""" @@ -573,7 +586,8 @@ class HomeInterface(QWidget): self.append_log_signal.disconnect(self.append_log) self.update_preview_with_comp_signal.disconnect(self.update_preview_with_comp) self.task_error_signal.disconnect(self.on_task_error) - + self.video_display_component.video_slider.valueChanged.disconnect(self.slider_changed) + self.video_display_component.ab_sections_changed.disconnect(self.ab_sections_changed) # 释放视频资源 if self.video_cap: self.video_cap.release() @@ -582,6 +596,6 @@ class HomeInterface(QWidget): # 确保所有子进程都已终止 ProcessManager.instance().terminate_all() except Exception as e: - print(f"关闭窗口时出错: {str(e)}") + print(f"Error during close window:", e) super().closeEvent(event) \ No newline at end of file