618 lines
23 KiB
Python
618 lines
23 KiB
Python
'''
|
|
This file is to inference arbitrary video files for Shot Cut
|
|
'''
|
|
import os, sys, shutil
|
|
import argparse
|
|
import numpy as np
|
|
import math
|
|
import subprocess
|
|
import cv2
|
|
import copy
|
|
import ffmpeg
|
|
import json
|
|
import torch
|
|
import torchvision.transforms as T
|
|
from torch.utils.data import DataLoader
|
|
from argparse import Namespace
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
import warnings
|
|
warnings.filterwarnings("ignore", category=UserWarning)
|
|
|
|
|
|
# Import files from the local folder
|
|
# 使用 __file__ 定位插件根目录,而不是用 '.' (ComfyUI 启动目录)
|
|
root_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
# 确保插件根目录始终位于 sys.path 最前面,
|
|
# 防止其他路径中存在同名的 config.py 单文件与插件的 config 包冲突
|
|
if root_path in sys.path:
|
|
sys.path.remove(root_path)
|
|
sys.path.insert(0, root_path)
|
|
|
|
# 清理可能被错误缓存的同名系统包(config、datasets 等),强制从插件根目录重新加载
|
|
for _mod_name in list(sys.modules.keys()):
|
|
if (
|
|
_mod_name == "config" or _mod_name.startswith("config.") or
|
|
_mod_name == "datasets" or _mod_name.startswith("datasets.")
|
|
):
|
|
del sys.modules[_mod_name]
|
|
|
|
# 插件内部依赖全部延迟到函数调用时再导入,避免 ComfyUI 启动阶段因
|
|
# 环境缺包/路径未就绪而抛出异常,进而导致 NODE_CLASS_MAPPINGS 未定义。
|
|
_omni_imports_done = False
|
|
def _ensure_imports():
|
|
global _omni_imports_done
|
|
global get_args_parser, build_backbone, build_transformer, OmniShotCut
|
|
global Video_Augmentation_Transform, visualize_concated_frames
|
|
global unique_intra_label_mapping, unique_inter_label_mapping
|
|
global intra_int2string, inter_int2string
|
|
global video_transform
|
|
|
|
if _omni_imports_done:
|
|
return
|
|
from config.argument_setting import get_args_parser
|
|
from architecture.backbone import build_backbone
|
|
from architecture.transformer import build_transformer
|
|
from architecture.model import OmniShotCut
|
|
from datasets.transforms import Video_Augmentation_Transform
|
|
from util.visualization import visualize_concated_frames
|
|
from config.label_correspondence import (
|
|
unique_intra_label_mapping, unique_inter_label_mapping,
|
|
intra_int2string, inter_int2string,
|
|
)
|
|
video_transform = Video_Augmentation_Transform(set_type="val")
|
|
_omni_imports_done = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_model(checkpoint_path: str):
|
|
_ensure_imports()
|
|
# 1. 检查路径
|
|
checkpoint_path = os.path.abspath(checkpoint_path)
|
|
if not os.path.exists(checkpoint_path):
|
|
raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
|
|
|
|
# 2. 加载 state_dict (修复 PyTorch 2.6 安全报错)
|
|
try:
|
|
# 显式设置 weights_only=False 以允许加载 argparse.Namespace 等对象
|
|
state_dict = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
|
except TypeError:
|
|
# 兼容不支持 weights_only 参数的旧版本 PyTorch
|
|
state_dict = torch.load(checkpoint_path, map_location="cpu")
|
|
|
|
if "args" not in state_dict or "model" not in state_dict:
|
|
raise ValueError("Checkpoint must contain keys: 'args' and 'model'.")
|
|
|
|
# 3. 初始化模型结构
|
|
model_args = state_dict["args"]
|
|
backbone = build_backbone(model_args)
|
|
transformer = build_transformer(model_args)
|
|
|
|
model = OmniShotCut(
|
|
backbone,
|
|
transformer,
|
|
num_intra_relation_classes = model_args.num_intra_relation_classes,
|
|
num_inter_relation_classes = model_args.num_inter_relation_classes,
|
|
num_frames = model_args.max_process_window_length,
|
|
num_queries = model_args.num_queries,
|
|
aux_loss = model_args.aux_loss,
|
|
)
|
|
|
|
# 4. 加载权重(保留在 CPU,由调用者负责 .to(device),避免重复移动导致显存OOM)
|
|
model.load_state_dict(state_dict["model"], strict=True)
|
|
model.eval()
|
|
|
|
return model, model_args
|
|
|
|
|
|
|
|
def get_video_fps_safe(video_path: str, default_fps: float = 24.0) -> float:
|
|
try:
|
|
cap = cv2.VideoCapture(video_path)
|
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
cap.release()
|
|
if fps is None or fps <= 1e-6 or math.isnan(fps):
|
|
return default_fps
|
|
return float(fps)
|
|
except Exception:
|
|
return default_fps
|
|
|
|
|
|
|
|
def split_videos(video, chunk_size, num_context_frames):
|
|
|
|
assert video.ndim == 4, "video must be (T, H, W, C)"
|
|
total_num_frames, H, W, C = video.shape
|
|
|
|
|
|
# Padding at the beginning
|
|
black = np.zeros((num_context_frames, H, W, C), dtype=video.dtype)
|
|
video = np.concatenate([black, video], axis=0)
|
|
|
|
|
|
# Split Video to clips
|
|
stride = chunk_size - 2 * num_context_frames
|
|
cur_frame_idx = 0
|
|
return_list = []
|
|
while cur_frame_idx < total_num_frames:
|
|
|
|
# Fetch the range
|
|
cropped_videos = video[cur_frame_idx : cur_frame_idx + chunk_size]
|
|
|
|
# Add padding if needed
|
|
clip_num_adding_frames = chunk_size - len(cropped_videos)
|
|
if clip_num_adding_frames > 0:
|
|
black = np.zeros((clip_num_adding_frames, H, W, C), dtype=video.dtype)
|
|
cropped_videos = np.concatenate([cropped_videos, black], axis=0)
|
|
|
|
# Append all return info: (video_np, clip padding frames, global start frame idx)
|
|
return_list.append([cropped_videos, clip_num_adding_frames])
|
|
|
|
# Update
|
|
cur_frame_idx += stride
|
|
|
|
return return_list
|
|
|
|
|
|
|
|
def prune_non_context_ranges(pred_ranges, pred_intra_labels, pred_inter_labels, inference_window_size, num_context_frames):
|
|
|
|
# Init
|
|
new_pred_ranges, new_pred_intra_labels, new_pred_inter_labels = [], [], []
|
|
|
|
|
|
# Iterate
|
|
for shot_idx in range(len(pred_ranges)):
|
|
|
|
# Fetch
|
|
start_frame_idx, end_frame_idx = pred_ranges[shot_idx]
|
|
|
|
# Check if we should skip
|
|
if end_frame_idx <= num_context_frames: # Beginning
|
|
continue
|
|
if start_frame_idx >= inference_window_size - num_context_frames: # Ending
|
|
break
|
|
|
|
# Re align start & end
|
|
aligned_start_frame_idx = max(start_frame_idx, num_context_frames) - num_context_frames
|
|
aligned_end_frame_idx = min(end_frame_idx, inference_window_size - num_context_frames) - num_context_frames # exclusive on the right range
|
|
|
|
# Append
|
|
new_pred_ranges.append([aligned_start_frame_idx, aligned_end_frame_idx])
|
|
new_pred_intra_labels.append(pred_intra_labels[shot_idx])
|
|
new_pred_inter_labels.append(pred_inter_labels[shot_idx])
|
|
|
|
return new_pred_ranges, new_pred_intra_labels, new_pred_inter_labels
|
|
|
|
|
|
|
|
def merge_ranges(pred_ranges_full, pred_intra_labels_full, pred_inter_labels_full, pred_ranges, pred_intra_labels, pred_inter_labels):
|
|
|
|
# Prepare
|
|
last_frame_idx = pred_ranges_full[-1][-1] if len(pred_intra_labels_full) != 0 else 0
|
|
|
|
|
|
# Merge last one of the list list
|
|
if len(pred_intra_labels_full) != 0 and pred_intra_labels_full[-1] == pred_intra_labels[0] and pred_inter_labels[0] == unique_inter_label_mapping['new_start']:
|
|
pred_ranges_full[-1][-1] = last_frame_idx + pred_ranges[0][-1]
|
|
|
|
# Crop the first one
|
|
pred_ranges = pred_ranges[1:]
|
|
pred_intra_labels = pred_intra_labels[1:]
|
|
pred_inter_labels = pred_inter_labels[1:]
|
|
|
|
|
|
# Extend the following list
|
|
for idx in range(len(pred_ranges)):
|
|
start_frame_idx, end_frame_idx = pred_ranges[idx]
|
|
|
|
pred_ranges_full.append([last_frame_idx + start_frame_idx, last_frame_idx + end_frame_idx])
|
|
pred_intra_labels_full.append(pred_intra_labels[idx])
|
|
pred_inter_labels_full.append(pred_inter_labels[idx])
|
|
|
|
|
|
return pred_ranges_full, pred_intra_labels_full, pred_inter_labels_full
|
|
|
|
|
|
|
|
def _get_ffmpeg_major_version() -> int:
|
|
"""获取系统 FFmpeg 主版本号,失败时返回 0。"""
|
|
try:
|
|
result = subprocess.run(
|
|
["ffmpeg", "-version"],
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
)
|
|
first_line = result.stdout.decode("utf-8", errors="ignore").splitlines()[0]
|
|
# 格式: "ffmpeg version 4.4.2-..." 或 "ffmpeg version 6.0-..."
|
|
ver_str = first_line.split("version")[1].strip().split()[0] # e.g. "4.4.2-0ubuntu..."
|
|
major = int(ver_str.split(".")[0])
|
|
return major
|
|
except Exception:
|
|
return 0
|
|
|
|
_FFMPEG_MAJOR = _get_ffmpeg_major_version()
|
|
|
|
|
|
def single_video_inference(video_path, model, model_args, num_context_frames, device=None):
|
|
_ensure_imports()
|
|
# 1. 强制转换为绝对路径,并且不要手动加引号(ffmpeg-python 会处理)
|
|
video_path = os.path.abspath(video_path)
|
|
|
|
# 自动推断设备:优先使用传入的 device,其次跟随模型参数所在设备,最后回退 cpu
|
|
if device is None:
|
|
try:
|
|
device = next(model.parameters()).device
|
|
except StopIteration:
|
|
device = torch.device("cpu")
|
|
|
|
# 调试打印,方便你在控制台看完整路径
|
|
print(f"## [OmniShotCut] FFmpeg 正在打开: {video_path} (FFmpeg major={_FFMPEG_MAJOR})")
|
|
|
|
process_height, process_width = model_args.process_height, model_args.process_width
|
|
max_process_window_length = model_args.max_process_window_length
|
|
|
|
# 2. 调用 FFmpeg
|
|
# fps_mode 是 FFmpeg 5.0+ 的参数;4.x 及以下用 vsync
|
|
try:
|
|
output_kwargs = dict(
|
|
format="rawvideo",
|
|
pix_fmt="rgb24",
|
|
s=f"{process_width}x{process_height}",
|
|
)
|
|
if _FFMPEG_MAJOR >= 5:
|
|
output_kwargs["fps_mode"] = "passthrough"
|
|
else:
|
|
output_kwargs["vsync"] = "passthrough"
|
|
|
|
process = (
|
|
ffmpeg
|
|
.input(filename=video_path)
|
|
.output("pipe:", **output_kwargs)
|
|
.run_async(pipe_stdout=True, pipe_stderr=True)
|
|
)
|
|
video_stream, err = process.communicate()
|
|
|
|
if process.returncode != 0:
|
|
raise ffmpeg.Error(None, video_stream, err)
|
|
|
|
except ffmpeg.Error as e:
|
|
stderr_msg = e.stderr.decode('utf-8') if e.stderr else "FFmpeg 启动失败"
|
|
print(f"## [OmniShotCut] FFmpeg 错误详情:\n{stderr_msg}")
|
|
raise RuntimeError(f"FFmpeg 无法打开视频。详情: {stderr_msg}")
|
|
|
|
# 3. 转换 Buffer
|
|
video_np_full = np.frombuffer(video_stream, np.uint8).reshape(-1, process_height, process_width, 3)
|
|
|
|
# 获取 FPS
|
|
fps = get_video_fps_safe(video_path)
|
|
|
|
# ... 后面的推理逻辑保持不变 ...
|
|
|
|
pred_ranges_full, pred_intra_labels_full, pred_inter_labels_full = [], [], []
|
|
|
|
# 注意:此处确保 video_transform 已经定义
|
|
for clip_idx, (video_np, num_pad_frames) in enumerate(split_videos(video_np_full, max_process_window_length, num_context_frames)):
|
|
|
|
# Transform & Move to same device as model
|
|
video_tensor = video_transform(video_np).unsqueeze(0).to(device)
|
|
|
|
# Inference
|
|
with torch.inference_mode():
|
|
outputs = model(video_tensor)
|
|
|
|
# 后处理逻辑
|
|
probas_intra = outputs['intra_clip_logits'].softmax(-1)[0, :, :-1]
|
|
probas_inter = outputs['inter_clip_logits'].softmax(-1)[0, :, :-1]
|
|
range_probas = outputs['pred_shot_logits'].softmax(-1)[0, :, :-1]
|
|
|
|
query_intra_idx = probas_intra.argmax(dim=-1)
|
|
query_inter_idx = probas_inter.argmax(dim=-1)
|
|
query_range_idx = range_probas.argmax(dim=-1)
|
|
|
|
pred_ranges, pred_intra_labels, pred_inter_labels = [], [], []
|
|
start_frame_idx = 0
|
|
|
|
for keep_idx in range(len(query_intra_idx)):
|
|
pred_intra_label = int(query_intra_idx[keep_idx].detach().cpu())
|
|
pred_inter_label = int(query_inter_idx[keep_idx].detach().cpu())
|
|
end_frame_idx = int(query_range_idx[keep_idx].detach().cpu())
|
|
|
|
pred_range = [start_frame_idx, end_frame_idx]
|
|
if start_frame_idx >= end_frame_idx:
|
|
continue
|
|
|
|
pred_ranges.append(pred_range)
|
|
pred_intra_labels.append(pred_intra_label)
|
|
pred_inter_labels.append(pred_inter_label)
|
|
start_frame_idx = end_frame_idx
|
|
|
|
if end_frame_idx >= max_process_window_length - num_pad_frames:
|
|
break
|
|
|
|
# 剪枝与合并
|
|
pred_ranges, pred_intra_labels, pred_inter_labels = prune_non_context_ranges(
|
|
pred_ranges, pred_intra_labels, pred_inter_labels, max_process_window_length, num_context_frames
|
|
)
|
|
|
|
pred_ranges_full, pred_intra_labels_full, pred_inter_labels_full = merge_ranges(
|
|
pred_ranges_full, pred_intra_labels_full, pred_inter_labels_full,
|
|
pred_ranges, pred_intra_labels, pred_inter_labels
|
|
)
|
|
|
|
return pred_ranges_full, pred_intra_labels_full, pred_inter_labels_full, video_np_full, fps
|
|
|
|
|
|
|
|
|
|
def dump_list_of_dict(data, save_path, indent=4):
|
|
"""
|
|
Save list[dict] as JSON.
|
|
Convert pred_intra_labels / pred_inter_labels from int to string.
|
|
"""
|
|
|
|
def convert_item(item):
|
|
item = copy.deepcopy(item)
|
|
|
|
if "pred_intra_labels" in item:
|
|
item["pred_intra_labels"] = [
|
|
intra_int2string.get(x, f"Unknown_{x}")
|
|
for x in item["pred_intra_labels"]
|
|
]
|
|
|
|
if "pred_inter_labels" in item:
|
|
item["pred_inter_labels"] = [
|
|
inter_int2string.get(x, f"Unknown_{x}")
|
|
for x in item["pred_inter_labels"]
|
|
]
|
|
|
|
return item
|
|
|
|
def format_dict(d, level):
|
|
indent_str = " " * (indent * level)
|
|
inner_indent = " " * (indent * (level + 1))
|
|
|
|
lines = [indent_str + "{"]
|
|
items = list(d.items())
|
|
|
|
for i, (k, v) in enumerate(items):
|
|
value_str = json.dumps(v, ensure_ascii=False)
|
|
comma = "," if i < len(items) - 1 else ""
|
|
lines.append(f'{inner_indent}"{k}": {value_str}{comma}')
|
|
|
|
lines.append(f"{indent_str}}}")
|
|
return "\n".join(lines)
|
|
|
|
data = [convert_item(item) for item in data]
|
|
|
|
with open(save_path, "w", encoding="utf-8") as f:
|
|
f.write("[\n")
|
|
|
|
for i, item in enumerate(data):
|
|
dict_str = format_dict(item, level=1)
|
|
comma = "," if i < len(data) - 1 else ""
|
|
f.write(dict_str + comma + "\n")
|
|
|
|
f.write("]\n")
|
|
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument(
|
|
"--checkpoint_path",
|
|
type = str,
|
|
default = "checkpoints/OmniShotCut_ckpt.pth",
|
|
help = "Path to checkpoint file."
|
|
)
|
|
parser.add_argument(
|
|
"--input_video_path",
|
|
type = str,
|
|
default = "__assets__/demo_video1.mp4",
|
|
help = "Path to the input video path."
|
|
)
|
|
parser.add_argument(
|
|
"--result_store_path",
|
|
type = str,
|
|
default = "results.json",
|
|
help="Path to save result json."
|
|
)
|
|
parser.add_argument(
|
|
"--num_context_frames",
|
|
type = int,
|
|
default = 0,
|
|
help = "Number of historical context frames to overlap between each clip window. The default is 100, using 5-10 should be good."
|
|
)
|
|
parser.add_argument(
|
|
"--visual_store_folder_path",
|
|
type = str,
|
|
default = "demo_video_results",
|
|
help = "Path to save the visualization results. Set to None to disable."
|
|
)
|
|
parser.add_argument(
|
|
"--mode",
|
|
type = str,
|
|
default = "default",
|
|
choices = ["default", "clean_shot"],
|
|
help = "Output Mode. 'default' means all Intra and Inter label. 'clean_shot' means only General Shot Cut without transitions. "
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
# Setting
|
|
inference_args = parse_args()
|
|
checkpoint_path = inference_args.checkpoint_path
|
|
input_video_path = inference_args.input_video_path
|
|
assert(os.path.exists(input_video_path))
|
|
result_store_path = inference_args.result_store_path
|
|
visual_store_folder_path = inference_args.visual_store_folder_path
|
|
mode = inference_args.mode
|
|
|
|
|
|
|
|
# Prepare the folder
|
|
if visual_store_folder_path is not None:
|
|
if os.path.exists(visual_store_folder_path):
|
|
shutil.rmtree(visual_store_folder_path)
|
|
os.makedirs(visual_store_folder_path)
|
|
|
|
|
|
# Load Checkpoint & Model Config
|
|
assert(os.path.exists(checkpoint_path))
|
|
state_dict = torch.load(checkpoint_path, map_location='cpu')
|
|
model_args = state_dict['args']
|
|
print("Checkpoint stored args are", model_args)
|
|
|
|
|
|
# Init the Model
|
|
print("Loading OmniShotCut Model!")
|
|
backbone = build_backbone(model_args)
|
|
transformer = build_transformer(model_args)
|
|
model = OmniShotCut(
|
|
backbone,
|
|
transformer,
|
|
num_intra_relation_classes = model_args.num_intra_relation_classes,
|
|
num_inter_relation_classes = model_args.num_inter_relation_classes,
|
|
num_frames = model_args.max_process_window_length,
|
|
num_queries = model_args.num_queries,
|
|
aux_loss = model_args.aux_loss,
|
|
)
|
|
model.load_state_dict(state_dict['model'], strict=True)
|
|
model.to("cuda")
|
|
model.eval()
|
|
|
|
|
|
|
|
|
|
# Do the inference
|
|
print("Do the inference!")
|
|
pred_ranges_full, pred_intra_labels_full, pred_inter_labels_full, video_np_full, fps = single_video_inference(input_video_path, model, model_args, inference_args.num_context_frames)
|
|
|
|
|
|
|
|
# Visualize
|
|
if visual_store_folder_path is not None:
|
|
print("Visualize the results!")
|
|
pred_saved_paths = visualize_concated_frames(video_np_full, visual_store_folder_path, pred_ranges_full, max_frames_per_img=264, end_range_exclusive=True, fps=24, start_index = 0)
|
|
|
|
|
|
|
|
# For Clean Shot mode, we only leave general types
|
|
if mode == "clean_shot":
|
|
|
|
# Clean shot mode (No Transitions in the middle)
|
|
general_type_idx = unique_intra_label_mapping["general"]
|
|
effective_indices = []
|
|
for idx, intra_label in enumerate(pred_intra_labels_full):
|
|
if intra_label == general_type_idx:
|
|
effective_indices.append(idx)
|
|
|
|
# Reassign the shots
|
|
pred_ranges_full = np.array(pred_ranges_full)[effective_indices].tolist()
|
|
pred_intra_labels_full = np.array(pred_intra_labels_full)[effective_indices].tolist()
|
|
pred_inter_labels_full = np.array(pred_inter_labels_full)[effective_indices].tolist()
|
|
|
|
|
|
|
|
# Collect prediction results
|
|
pred_result = {}
|
|
pred_result["video_path"] = input_video_path
|
|
pred_result["pred_ranges"] = pred_ranges_full
|
|
pred_result["pred_intra_labels"] = pred_intra_labels_full
|
|
pred_result["pred_inter_labels"] = pred_inter_labels_full
|
|
|
|
# Dump to json
|
|
dump_list_of_dict([pred_result], result_store_path)
|
|
|
|
|
|
print("Finished!")
|
|
|
|
# 包装类,方便 ComfyUI 调用
|
|
class OmniShotCutDetector:
|
|
def __init__(self, checkpoint_path, device="cuda"):
|
|
self.model, self.model_args = load_model(checkpoint_path)
|
|
|
|
# 优先使用 ComfyUI 的模型管理器获取推荐设备,
|
|
# 它会处理多卡、显存限制、CPU offload 等复杂情况
|
|
try:
|
|
import comfy.model_management as mm
|
|
target_device = mm.get_torch_device()
|
|
except Exception:
|
|
target_device = torch.device(device)
|
|
|
|
# 将模型移至目标设备,若失败则自动降级到 CPU
|
|
try:
|
|
self.model.to(target_device)
|
|
self.device = target_device
|
|
except (torch.cuda.OutOfMemoryError, RuntimeError) as e:
|
|
print(f"## [OmniShotCut] 警告:无法将模型加载到 {target_device},自动降级到 CPU。原因: {e}")
|
|
self.model.to("cpu")
|
|
self.device = torch.device("cpu")
|
|
|
|
print(f"## [OmniShotCut] 模型已加载到设备: {self.device}")
|
|
|
|
def run_inference(self, input_video_path, mode="default", num_context_frames=0):
|
|
# 调用原始推理函数,传入 device 确保张量与模型在同一设备上
|
|
pred_ranges, pred_intra_labels, pred_inter_labels, video_np, fps = single_video_inference(
|
|
input_video_path, self.model, self.model_args, num_context_frames, device=self.device
|
|
)
|
|
|
|
# ── 步骤1:先生成预览图(使用完整镜头列表,与原项目 inference.py 第427行一致)
|
|
vis_images = []
|
|
try:
|
|
import tempfile
|
|
tmp_dir = tempfile.mkdtemp(prefix="omni_preview_")
|
|
saved_paths = visualize_concated_frames(
|
|
frames=video_np,
|
|
out_dir=tmp_dir,
|
|
highlight_ranges_closed=pred_ranges,
|
|
max_frames_per_img=264,
|
|
end_range_exclusive=True,
|
|
fps=24, # 固定24fps,与原项目保持一致
|
|
start_index=0,
|
|
)
|
|
for p in saved_paths:
|
|
vis_images.append(Image.open(p).copy()) # copy() 防止文件句柄占用
|
|
# 清理临时文件
|
|
for p in saved_paths:
|
|
try:
|
|
os.remove(p)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
os.rmdir(tmp_dir)
|
|
except Exception:
|
|
pass
|
|
except Exception as e:
|
|
import traceback
|
|
print(f"## [OmniShotCut] 预览图生成失败: {e}")
|
|
traceback.print_exc()
|
|
|
|
# ── 步骤2:clean_shot 模式过滤(仅影响 JSON 输出,不影响预览图)
|
|
if mode == "clean_shot":
|
|
general_type_idx = unique_intra_label_mapping["general"]
|
|
effective_indices = [i for i, lbl in enumerate(pred_intra_labels) if lbl == general_type_idx]
|
|
pred_ranges = [pred_ranges[i] for i in effective_indices]
|
|
pred_intra_labels = [pred_intra_labels[i] for i in effective_indices]
|
|
pred_inter_labels = [pred_inter_labels[i] for i in effective_indices]
|
|
|
|
# ── 步骤3:结果打包,int 标签转字符串,列表包裹,与原项目 output.json 格式一致
|
|
results = [
|
|
{
|
|
"video_path": input_video_path,
|
|
"pred_ranges": pred_ranges,
|
|
"pred_intra_labels": [intra_int2string.get(x, f"Unknown_{x}") for x in pred_intra_labels],
|
|
"pred_inter_labels": [inter_int2string.get(x, f"Unknown_{x}") for x in pred_inter_labels],
|
|
}
|
|
]
|
|
|
|
return results, vis_images # vis_images 为 PIL Image 列表 |