[Python] 纯文本查看 复制代码
import os
import subprocess
import re
import json
import secrets
import tempfile
import time
import threading
from typing import Optional, Tuple, Dict, List
# ====================== 配置项 ======================
input_dir = "/storage/emulated/0/视频分割/"
output_dir = os.path.join(input_dir, "分割完成")
frame_dir = "/storage/emulated/0/JPEG截帧/"
video_extensions = (".mp4", ".mkv", ".mov", ".avi", ".flv", ".webm")
image_extensions = (".jpg", ".jpeg", ".png", ".bmp", ".webp")
last_custom_fps = None
# ★ 存储格式:(clip_path, start_sec, dur, out_base)
clip_history_list = []
_color_params_cache = None
_cached_target_path = None
_target_time_cache = None
# 运行期间记忆
_last_tonemap = True
_nf_preset = False
last_scheme_code = None
_first_frame_extract_flag = True # 控制滤镜信息只打印一次
_global_remembered_fps = None # 全终端帧率记忆
# ====================== 原子唯一签名注入器 ======================
class AtomicUniqueInjector:
def __init__(self):
self._counter = 0
self._lock = threading.Lock()
self._instance_id = secrets.token_bytes(16)
def inject(self, file_path):
with self._lock:
self._counter += 1
nano_time = time.time_ns()
counter_bytes = self._counter.to_bytes(8, 'big')
random_bytes = secrets.token_bytes(32)
unique_signature = (
self._instance_id +
nano_time.to_bytes(8, 'big') +
counter_bytes +
random_bytes
)
try:
with open(file_path, "ab", buffering=0) as f:
f.write(unique_signature)
return True
except Exception:
return False
_unique_injector = AtomicUniqueInjector()
def check_exit(s):
txt = s.strip().lower()
if txt in ["退出", "tc"]:
print("\n👋 程序已强制退出,所有设置未保存")
os._exit(0)
def fast_modify_md5(file_path):
try:
_unique_injector.inject(file_path)
except Exception:
pass
# ====================== LLC 时间格式化函数 ======================
def sec_to_time_llc(total_sec):
ms_total = int(total_sec * 1000)
hour = ms_total // 3600000
remain = ms_total % 3600000
minute = remain // 60000
remain = remain % 60000
second = remain // 1000
millisec = remain % 1000
return f"{hour:02d}.{minute:02d}.{second:02d}.{millisec:03d}"
# ==============================================
# 视频参数探测
# ==============================================
def probe_color_params(video_path: str) -> Dict:
cmd = [
"ffprobe", "-hide_banner",
"-select_streams", "v:0",
"-show_entries",
"stream=color_primaries,color_transfer,color_space,pix_fmt,width,height,r_frame_rate,bit_depth,color_range",
"-of", "json",
video_path
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
data = json.loads(result.stdout)
streams = data.get("streams", [])
if not streams:
return {"error": "no stream"}
stream = streams[0]
color_range = stream.get("color_range", "unknown").lower()
return {
"color_primaries": stream.get("color_primaries", "unknown"),
"color_transfer": stream.get("color_transfer", "unknown"),
"color_space": stream.get("color_space", "unknown"),
"pix_fmt": stream.get("pix_fmt", "unknown"),
"width": stream.get("width", 0),
"height": stream.get("height", 0),
"r_frame_rate": stream.get("r_frame_rate", "24000/1001"),
"bit_depth": stream.get("bit_depth", 8),
"color_range": color_range,
}
except Exception as e:
return {"error": str(e)}
def is_hdr_video(params: Dict) -> bool:
cp = params.get("color_primaries", "").lower()
ct = params.get("color_transfer", "").lower()
cs = params.get("color_space", "").lower()
pf = params.get("pix_fmt", "").lower()
hdr_keywords = ["bt2020", "smpte2084", "arib-std-b67", "pq", "hlg"]
if any(k in cp or k in ct or k in cs for k in hdr_keywords):
return True
if ("10le" in pf or "12le" in pf) and ("bt2020" in cs or "smpte" in ct):
return True
return False
def show_color_info(video_path):
params = probe_color_params(video_path)
if "error" in params:
print(f"⚠️ 无法获取色彩参数: {params['error']}")
return None
print("\n📊 原始视频色彩参数:")
print(f" 色彩空间 : {params.get('color_space', '?')}")
print(f" 色彩原色 : {params.get('color_primaries', '?')}")
print(f" 传输特性 : {params.get('color_transfer', '?')}")
print(f" 像素格式 : {params.get('pix_fmt', '?')}")
print(f" 位深 : {params.get('bit_depth', '?')} bit")
print(f" 分辨率 : {params.get('width')}x{params.get('height')}")
print(f" 色彩范围 : {params.get('color_range', '?')}")
print(f" HDR : {'是' if is_hdr_video(params) else '否'}")
return params
# ==============================================
# 色彩匹配
# ==============================================
def find_target_color_file(exclude_file=None):
if not os.path.exists(input_dir):
return None
all_extensions = video_extensions + image_extensions
for f in sorted(os.listdir(input_dir)):
fp = os.path.join(input_dir, f)
if "分割完成" in fp or "分割" in f:
continue
if os.path.isfile(fp) and f.lower().endswith(all_extensions):
if exclude_file and f == exclude_file:
continue
return fp
return None
def is_image_file(file_path):
return file_path.lower().endswith(image_extensions)
def input_target_time_range(target_path):
global _target_time_cache
if is_image_file(target_path):
print("📷 目标为图片文件,直接采样")
return 0, 1
duration = get_video_duration(target_path)
while True:
u = input("\n🎨 色彩采样时间范围(格式:MM:SS-MM:SS 或 回车=整个视频):").strip()
check_exit(u)
if not u:
_target_time_cache = (0, duration)
return 0, duration
parts = u.split('-')
if len(parts) != 2:
print("⚠️ 格式:开始时间-结束时间(如:00:30-01:30)")
continue
start_str, end_str = parts[0].strip(), parts[1].strip()
if not validate_time(start_str) or not validate_time(end_str):
print("⚠️ 时间格式错误,使用 MM:SS 或 HH:MM:SS")
continue
start_sec = time_to_sec(start_str)
end_sec = time_to_sec(end_str)
if start_sec >= end_sec:
print("❌ 结束时间必须大于开始时间")
continue
if end_sec > duration:
end_sec = duration
_target_time_cache = (start_sec, end_sec)
return start_sec, end_sec
def get_color_params(file_path, target_path=None, start_sec=0, duration=None):
global _color_params_cache, _cached_target_path, _target_time_cache
if target_path is None:
target_path = find_target_color_file()
if not target_path:
return (128, 128, 128)
if file_path == target_path and _color_params_cache is not None and _cached_target_path == target_path:
return _color_params_cache
is_img = is_image_file(target_path)
tmp_raw = os.path.join(tempfile.gettempdir(), f"color_{secrets.token_hex(4)}.raw")
if is_img:
cmd_sample = [
"ffmpeg", "-hide_banner", "-loglevel", "error",
"-i", target_path,
"-vf", "scale=1:1:flags=area,format=rgb24",
"-vframes", "1",
"-f", "rawvideo",
"-y", tmp_raw
]
else:
if duration is None:
duration = min(3, get_video_duration(target_path))
sample_count = min(10, int(duration * 2))
cmd_sample = [
"ffmpeg", "-hide_banner", "-loglevel", "error",
"-ss", str(start_sec),
"-i", target_path,
"-t", str(duration),
"-vf", "scale=1:1:flags=area,format=rgb24",
"-vsync", "0",
"-vframes", str(sample_count),
"-f", "rawvideo",
"-y", tmp_raw
]
subprocess.run(cmd_sample, capture_output=True)
r_sum = g_sum = b_sum = 0
count = 0
if os.path.exists(tmp_raw):
with open(tmp_raw, "rb") as f:
data = f.read()
os.remove(tmp_raw)
for i in range(0, len(data), 3):
if i + 2 < len(data):
r_sum += data[i]
g_sum += data[i+1]
b_sum += data[i+2]
count += 1
if count == 0:
return (128, 128, 128)
r_avg = r_sum // count
g_avg = g_sum // count
b_avg = b_sum // count
if file_path == target_path:
_color_params_cache = (r_avg, g_avg, b_avg)
_cached_target_path = target_path
return (r_avg, g_avg, b_avg)
def build_color_match_filter(source_video, target_path=None):
if target_path is None:
target_path = find_target_color_file()
if not target_path:
return None
src_r, src_g, src_b = get_color_params(source_video)
tgt_r, tgt_g, tgt_b = get_color_params(target_path, target_path=target_path)
diff_r = tgt_r - src_r
diff_g = tgt_g - src_g
diff_b = tgt_b - src_b
if abs(diff_r) < 5 and abs(diff_g) < 5 and abs(diff_b) < 5:
return None
return (diff_b, diff_g, diff_r)
def setup_color_target(exclude_file=None):
global _color_params_cache, _cached_target_path, _target_time_cache
all_extensions = video_extensions + image_extensions
all_files = []
for f in sorted(os.listdir(input_dir)):
fp = os.path.join(input_dir, f)
if "分割完成" in fp or "分割" in f:
continue
if os.path.isfile(fp) and f.lower().endswith(all_extensions):
if exclude_file and f == exclude_file:
continue
all_files.append(f)
if len(all_files) <= 1:
_color_params_cache = None
_cached_target_path = None
_target_time_cache = None
return None
target_path = find_target_color_file(exclude_file)
if not target_path:
return None
_color_params_cache = None
_cached_target_path = None
_target_time_cache = None
start_sec, end_sec = input_target_time_range(target_path)
get_color_params(target_path, target_path=target_path, start_sec=start_sec, duration=end_sec-start_sec)
return target_path
# ==============================================
# 时间处理
# ==============================================
def time_to_sec(time_str):
t = time_str.strip()
if t.isdigit():
if len(t) == 1 or len(t) == 2:
return -1
if len(t) == 3 or len(t) == 4:
t = t.zfill(4)
t = f"{t[:2]}:{t[2:]}"
elif len(t) == 5 or len(t) == 6:
t = t.zfill(6)
t = f"{t[:2]}:{t[2:4]}:{t[4:]}"
else:
return -1
try:
parts = list(map(float, t.split(':')))
if len(parts) == 2:
m, s = parts
h = 0
elif len(parts) == 3:
h, m, s = parts
else:
return -1
return h * 3600 + m * 60 + s
except:
return -1
def validate_time(time_str):
t = time_str.strip()
pat = r'^\d{1,2}:\d{1,2}(\.\d+)?$|^\d{1,2}:\d{1,2}:\d{1,2}(\.\d+)?$'
if re.match(pat, t):
return True
if t.isdigit() and 3 <= len(t) <= 6:
return True
return False
# ==============================================
# 基础功能函数
# ==============================================
def get_fps_fraction(video_path):
cmd = [
"ffprobe", "-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=r_frame_rate",
"-of", "noprint_wrappers=1:nokey=1",
video_path
]
res = subprocess.run(cmd, capture_output=True, text=True)
raw = res.stdout.strip()
if "/" in raw:
a_str, b_str = raw.split("/")
a = int(a_str)
b = int(b_str)
else:
a = 24000
b = 1001
return a, b
def get_video_duration(video_path):
cmd = [
"ffprobe", "-v", "error",
"-show_entries", "format=duration",
"-of", "csv=p=0",
video_path
]
res = subprocess.run(cmd, capture_output=True, text=True)
try:
return float(res.stdout.strip())
except:
return 0
def get_video_fps_display(video_path):
a, b = get_fps_fraction(video_path)
return round(a / b)
def check_ffmpeg():
try:
subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True, check=True)
return True
except:
return False
# ==============================================
# 文件扫描与选择
# ==============================================
def scan_original_videos():
res = []
for f in os.listdir(input_dir):
fp = os.path.join(input_dir, f)
if (os.path.isfile(fp) and "分割完成" not in fp
and "分割" not in f and f.lower().endswith(video_extensions)):
res.append(f)
return sorted(res)
def select_video(target_path=None):
while True:
lst = scan_original_videos()
if not lst:
u = input("\n无视频,放入视频后回车 / 输入tc退出 / qbjz扫描全部视频:")
check_exit(u)
if u.strip().lower() == "qbjz":
qbjz_scan_input_dir_and_process(target_path)
continue
continue
if len(lst) == 1:
v = lst[0]
p = os.path.join(input_dir, v)
name, ext = os.path.splitext(v)
print(f"\n已自动选中:{v}")
return v, p, name, ext
print("\n===== 视频列表 =====")
for i, v in enumerate(lst):
print(f"{i+1}. {v}")
while True:
c = input("\n输入序号选择 | tc退出 | qbjz扫描全部视频:")
check_exit(c)
if c.strip().lower() == "qbjz":
qbjz_scan_input_dir_and_process(target_path)
continue
if not c.isdigit():
print("请输入数字")
continue
idx = int(c) - 1
if 0 <= idx < len(lst):
v = lst[idx]
p = os.path.join(input_dir, v)
name, ext = os.path.splitext(v)
print(f"\n已选中:{v}")
return v, p, name, ext
def parse_selection(user_input, max_num):
user_input = user_input.strip()
if not user_input:
return None
if '-' in user_input:
parts = user_input.split('-')
if len(parts) != 2:
return None
try:
start = int(parts[0].strip())
end = int(parts[1].strip())
except ValueError:
return None
if start < 1 or end < 1 or start > end or end > max_num:
return None
return list(range(start-1, end))
if ',' in user_input:
parts = user_input.split(',')
indices = []
for p in parts:
p = p.strip()
if not p.isdigit():
return None
num = int(p)
if num < 1 or num > max_num:
return None
indices.append(num-1)
return sorted(list(set(indices)))
if user_input.isdigit():
num = int(user_input)
if 1 <= num <= max_num:
return [num-1]
return None
def select_video_from_output():
if not os.path.exists(output_dir):
print(f"⚠️ 输出目录不存在: {output_dir}")
return []
videos = []
for f in os.listdir(output_dir):
fp = os.path.join(output_dir, f)
if os.path.isfile(fp) and f.lower().endswith(video_extensions):
videos.append(f)
if not videos:
print("⚠️ 分割完成目录中没有视频文件")
return []
videos = sorted(videos)
if len(videos) == 1:
v = videos[0]
p = os.path.join(output_dir, v)
name, ext = os.path.splitext(v)
print(f"\n自动选中切割后视频:{v}")
return [(v, p, name, ext)]
print("\n===== 已切割视频列表 =====")
for i, v in enumerate(videos):
print(f"{i+1}. {v}")
print("支持输入:单个序号(1) 范围(1-3) 列表(1,3,5)")
while True:
c = input("\n输入选择 | tc退出:")
check_exit(c)
indices = parse_selection(c, len(videos))
if indices is None:
print("❌ 无效输入,请重新输入")
continue
selected = []
for idx in indices:
v = videos[idx]
p = os.path.join(output_dir, v)
name, ext = os.path.splitext(v)
selected.append((v, p, name, ext))
print(f"\n已选中 {len(selected)} 个视频:")
for v, _, _, _ in selected:
print(f" - {v}")
return selected
# ==============================================
# 全局帧率记忆
# ==============================================
def input_custom_fps(video_path=None):
global last_custom_fps, _global_remembered_fps
if video_path:
real_fps = get_video_fps_display(video_path)
print(f"👉 原生帧率:{real_fps}fps")
if _global_remembered_fps is not None:
print(f"✅ 沿用已设置的帧率:{_global_remembered_fps}fps")
u = input("自定义抽帧帧率(1-120):").strip()
check_exit(u)
if not u:
last_custom_fps = _global_remembered_fps
return _global_remembered_fps
if u.isdigit() and 1 <= int(u) <= 120:
new_fps = int(u)
_global_remembered_fps = new_fps
last_custom_fps = new_fps
print(f"✅ 帧率已更新为:{new_fps}fps")
return new_fps
else:
print("❌ 仅1-120纯数字,保持原帧率")
return _global_remembered_fps
while True:
u = input("自定义抽帧帧率(1-120):").strip()
check_exit(u)
if not u and last_custom_fps is not None:
_global_remembered_fps = last_custom_fps
print(f"👉 已自动沿用上次帧率:{last_custom_fps}fps")
return last_custom_fps
if u.isdigit() and 1 <= int(u) <= 120:
new_fps = int(u)
_global_remembered_fps = new_fps
last_custom_fps = new_fps
print(f"✅ 帧率已设置为:{new_fps}fps")
return new_fps
print("❌ 仅1-120纯数字,重新输入:")
# ==============================================
# FFmpeg截帧(★ 关键修正:使用原始帧间隔 native_step)
# ==============================================
def extract_frames_ffmpeg(video_path, base_name, out_fps, start_sec=0, target_path=None, pix_fmt=None):
"""
无论 out_fps 为何值,文件名时间戳始终按原始视频的帧间隔递增(与原始帧时刻对齐)
"""
global _first_frame_extract_flag
# 获取原始视频的帧间隔(时间步长)
fps_num, fps_den = get_fps_fraction(video_path)
native_step = fps_den / fps_num # 原始每帧的时间增量
duration = get_video_duration(video_path)
if duration <= 0:
print(f"❌ 无法获取视频时长: {video_path}")
return False
if pix_fmt is None:
params = probe_color_params(video_path)
pix_fmt = params.get("pix_fmt", "yuv420p") if params else "yuv420p"
color_offset = None
if target_path:
color_offset = build_color_match_filter(video_path, target_path)
out_dir = os.path.join(frame_dir, base_name)
os.makedirs(out_dir, exist_ok=True)
# 构建滤镜链
filter_parts = [f"fps={out_fps}"]
if color_offset:
b_offset, g_offset, r_offset = color_offset
filter_parts.append(f"lutrgb=r='val+{r_offset}':g='val+{g_offset}':b='val+{b_offset}'")
filter_parts.append("scale=out_range=pc:in_range=tv:in_color_matrix=bt709:out_color_matrix=bt709")
filter_parts.append(f"format={pix_fmt}")
vf_filter = ",".join(filter_parts)
if _first_frame_extract_flag:
print(f"🔧 滤镜:{vf_filter}")
_first_frame_extract_flag = False
temp_pattern = os.path.join(out_dir, f"temp_%06d.jpeg")
cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel", "info",
"-i", video_path,
"-vf", vf_filter,
"-vsync", "vfr",
"-q:v", "1",
"-frame_pts", "1",
temp_pattern,
"-y"
]
print(f"🎬 处理片段:{base_name} 总时长:{duration:.1f}s")
print("⏳ 正在截帧,文件会实时生成...")
start_time = time.time()
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
if result.returncode != 0:
print(f"❌ FFmpeg执行失败: {result.stderr}")
return False
temp_files = sorted([f for f in os.listdir(out_dir) if f.startswith("temp_") and f.endswith(".jpeg")])
if not temp_files:
print(f"❌ 未生成帧图")
return False
frame_count = 0
last_timestamp = start_sec
for i, temp_file in enumerate(temp_files):
# ★ 使用原始帧间隔计算时间戳,与 out_fps 完全无关
timestamp = start_sec + i * native_step
time_str = sec_to_time_llc(timestamp)
new_filename = f"{base_name}-{time_str}.jpeg"
old_path = os.path.join(out_dir, temp_file)
new_path = os.path.join(out_dir, new_filename)
os.rename(old_path, new_path)
fast_modify_md5(new_path)
frame_count += 1
last_timestamp = timestamp
cost = round(time.time() - start_time, 1)
print(f"✅ {base_name} 完成!共 {frame_count} 张 | 耗时 {cost}秒")
print(f"🕐 时间范围:{sec_to_time_llc(start_sec)} → {sec_to_time_llc(last_timestamp)}")
print("🔐 每张图片已注入唯一签名")
print()
return True
except subprocess.TimeoutExpired:
print("❌ FFmpeg执行超时")
return False
except Exception as e:
print(f"❌ 截帧过程出错: {e}")
return False
# ==============================================
# 全部截帧模式 (qbjz)
# ==============================================
def qbjz_mode(video_path, file_name, target_path=None):
print("\n" + "=" * 40)
print(" 🎯 全部截帧模式 (qbjz) - 帧率抽帧")
print("=" * 40)
duration = get_video_duration(video_path)
a, b = get_fps_fraction(video_path)
real_fps = a / b
print(f"视频:{file_name} 时长:{duration:.2f}秒 原生帧率:{real_fps:.2f}fps")
base_name = os.path.splitext(file_name)[0]
params = probe_color_params(video_path)
pix_fmt = params.get("pix_fmt", "yuv420p") if params else "yuv420p"
while True:
out_fps = input_custom_fps(video_path)
extract_frames_ffmpeg(video_path, base_name, out_fps, 0, target_path, pix_fmt)
u = input("\n继续截帧回车 | n重新选片:").strip().lower()
check_exit(u)
if u == 'n':
break
# ==============================================
# 批量截帧(用于切割后的片段)
# ==============================================
def _run_batch_ffmpeg(out_fps, target_path, pix_fmt=None):
global clip_history_list
total_clips = len(clip_history_list)
# clip_history_list: (clip_path, start_sec, dur, out_base)
for clip_path, start_sec, dur, out_base in clip_history_list:
extract_frames_ffmpeg(clip_path, out_base, out_fps, start_sec, target_path, pix_fmt)
print(f"\n🎉 全部完成!共 {total_clips} 个片段")
clip_history_list.clear()
def batch_extract_frames(target_path=None):
global clip_history_list, last_custom_fps
if not clip_history_list:
print("⚠️ 暂无已分割的视频片段")
return
first_clip_path = clip_history_list[0][0]
src_params_first = show_color_info(first_clip_path)
if src_params_first is None:
return
out_fps = _get_fps(first_clip_path)
pix_fmt = src_params_first.get("pix_fmt", "yuv420p")
_run_batch_ffmpeg(out_fps, target_path, pix_fmt)
def _get_fps(video_path):
global last_custom_fps
custom_fps = input_custom_fps(video_path)
if custom_fps:
last_custom_fps = custom_fps
return custom_fps
a, b = get_fps_fraction(video_path)
return a / b
# ==============================================
# 视频自动编号逻辑
# ==============================================
def find_max_existing_number(prefix, file_ext):
if not os.path.exists(output_dir):
return 0
max_num = 0
escaped_prefix = re.escape(prefix)
escaped_ext = re.escape(file_ext)
pattern = re.compile(f'^{escaped_prefix}(\\d+){escaped_ext}$')
for f in os.listdir(output_dir):
match = pattern.match(f)
if match:
num = int(match.group(1))
if num > max_num:
max_num = num
return max_num
def get_next_output_filename(file_name, file_ext):
if file_name.endswith("-"):
base = file_name.rstrip("-")
max_num = find_max_existing_number(base + "-", file_ext)
num = max_num + 1
out_filename = f"{base}-{num:02d}{file_ext}"
out_path = os.path.join(output_dir, out_filename)
return out_filename, out_path
num_reg = re.compile(r'(\d+)$')
num_match = num_reg.search(file_name)
if num_match:
prefix = file_name[:num_match.start()]
original_num_str = num_match.group(1)
digit_len = len(original_num_str)
max_num = find_max_existing_number(prefix, file_ext)
current_num = max(1, max_num + 1)
original_num = int(original_num_str)
current_num = max(current_num, original_num)
out_filename = f"{prefix}{current_num:0{digit_len}d}{file_ext}"
out_path = os.path.join(output_dir, out_filename)
return out_filename, out_path
out_filename = f"{file_name}{file_ext}"
out_path = os.path.join(output_dir, out_filename)
if os.path.exists(out_path):
num = 1
while True:
out_filename = f"{file_name} {num:02d}{file_ext}"
out_path = os.path.join(output_dir, out_filename)
if not os.path.exists(out_path):
break
num += 1
return out_filename, out_path
# ====================== qbjz扫描全部目录 ======================
def qbjz_scan_input_dir_and_process(target_path):
all_videos = []
for root, dirs, files in os.walk(input_dir):
for f in files:
fp = os.path.join(root, f)
if f.lower().endswith(video_extensions):
all_videos.append(fp)
all_videos.sort()
if not all_videos:
print("⚠️ 目录及所有子目录中未发现视频文件。")
return
print("\n===== 扫描到的全部视频(含子目录) =====")
for i, v in enumerate(all_videos):
print(f"{i+1}. {os.path.basename(v)}")
while True:
c = input("\n输入选择(支持多选,1-3 或 1,3,5)| tc退出:").strip()
check_exit(c)
if c.lower() == "tc":
return
indices = parse_selection(c, len(all_videos))
if indices is None:
print("❌ 无效输入,请重新输入")
continue
selected_paths = [all_videos[i] for i in indices]
print(f"\n已选中 {len(selected_paths)} 个视频")
break
if len(selected_paths) == 1:
v_path = selected_paths[0]
v_name = os.path.basename(v_path)
qbjz_mode(v_path, v_name, target_path)
else:
first_video_path = selected_paths[0]
out_fps = input_custom_fps(first_video_path)
for v_path in selected_paths:
v_name = os.path.basename(v_path)
base_name = os.path.splitext(v_name)[0]
print(f"\n🎬 正在处理:{v_name}")
extract_frames_ffmpeg(v_path, base_name, out_fps, 0, target_path)
print(f"\n🎉 全部 {len(selected_paths)} 个视频截帧完成!")
# ====================== 主程序 ======================
if __name__ == "__main__":
os.makedirs(input_dir, exist_ok=True)
os.makedirs(output_dir, exist_ok=True)
os.makedirs(frame_dir, exist_ok=True)
if not check_ffmpeg():
print("❌ 未检测到FFmpeg")
input("回车退出...")
exit()
print("=" * 50)
print(" 视频分割 + FFmpeg 截帧 v10.0 (原始帧间隔命名)")
print("=" * 50)
target_path = setup_color_target()
if target_path:
print(f"🎨 色彩匹配已自动启用 → {os.path.basename(target_path)}")
else:
print("未检测到色彩目标,色彩匹配关闭")
print("=" * 50)
while True:
_, video_path, file_name, file_ext = select_video(target_path)
total_duration = get_video_duration(video_path)
while True:
start = input("\n开始时间: ").strip()
check_exit(start)
if start.lower() == "qbjz":
selected_videos = select_video_from_output()
if not selected_videos:
print("返回视频选择...")
break
if len(selected_videos) == 1:
v_name, v_path, v_base, v_ext = selected_videos[0]
qbjz_mode(v_path, v_name, target_path)
else:
print(f"\n📋 将对 {len(selected_videos)} 个视频统一截帧")
first_video_path = selected_videos[0][1]
out_fps = input_custom_fps(first_video_path)
params_first = probe_color_params(first_video_path)
pix_fmt = params_first.get("pix_fmt", "yuv420p") if params_first else "yuv420p"
if _first_frame_extract_flag:
print(f"🔧 滤镜:fps={out_fps},scale=out_range=pc:in_range=tv:in_color_matrix=bt709:out_color_matrix=bt709,format={pix_fmt}")
_first_frame_extract_flag = False
for v_name, v_path, v_base, v_ext in selected_videos:
print(f"\n🎬 正在处理:{v_name}")
extract_frames_ffmpeg(v_path, v_base, out_fps, 0, target_path, pix_fmt)
print(f"\n🎉 全部 {len(selected_videos)} 个视频截帧完成!")
break
if start.lower() == "n":
print("\n🔄 切换视频...")
break
if start == "":
if clip_history_list:
batch_extract_frames(target_path)
while True:
u = input("\n回车继续分割视频 | tc=退出:").strip().lower()
check_exit(u)
if u == "":
break
else:
print("⚠️ 回车继续 / tc退出")
break
else:
print("⚠️ 请先输入时间范围")
continue
if not validate_time(start):
print("❌ 无效输入,请使用 MM:SS / HH:MM:SS 或纯数字格式")
continue
s_sec = time_to_sec(start)
if s_sec < 0:
print("❌ 时间格式错误")
continue
if s_sec >= total_duration:
print("❌ 开始时间超出视频长度")
continue
end = input("结束时间: ").strip()
check_exit(end)
if end.lower() == "n":
print("\n🔄 切换视频...")
break
if end == "":
if clip_history_list:
batch_extract_frames(target_path)
while True:
u = input("\n回车继续分割视频 | tc=退出:").strip().lower()
check_exit(u)
if u == "":
break
else:
print("⚠️ 回车继续 / tc退出")
break
else:
print("⚠️ 尚无已分割的片段,请输入正确的结束时间进行切割")
continue
if not validate_time(end):
print("❌ 无效的结束时间格式")
continue
e_sec = time_to_sec(end)
if e_sec <= s_sec:
print("❌ 结束时间必须大于开始时间")
continue
dur = e_sec - s_sec
out_filename, out_path = get_next_output_filename(file_name, file_ext)
print(f"\n✂️ 正在切割:{out_filename}")
cmd = f'ffmpeg -hide_banner -loglevel error -ss {start} -i "{video_path}" -t {dur} -c copy -movflags +faststart -y "{out_path}"'
if subprocess.run(cmd, shell=True).returncode == 0:
if os.path.exists(out_path) and os.path.getsize(out_path) > 1024:
print("✅ 切割完成")
out_base = os.path.splitext(out_filename)[0]
# ★ 存储片段信息,包含起始时间
clip_history_list.append((out_path, s_sec, dur, out_base))
print(f"📋 累计片段:{len(clip_history_list)} → {out_base}")
else:
print("❌ 切割失败:输出文件异常")
else:
print("❌ 切割失败") 改一下吗🥹