This commit is contained in:
Admin 2025-10-28 16:02:55 +08:00
commit d74b66b95e
9 changed files with 1475 additions and 0 deletions

325
videos质检程序.py Normal file
View File

@ -0,0 +1,325 @@
import os
import sys
import subprocess
import tkinter as tk
from tkinter import messagebox
from datetime import datetime
from send2trash import send2trash # 需提前安装pip install send2trash
# =================================================================
# 请在这里修改为您的实际路径!
# =================================================================
# F 盘视频文件和此Python脚本所在的目录自动获取无需手动改
F_DISK_VIDEO_DIR = os.path.dirname(os.path.abspath(__file__))
# E 盘与视频同名的文件夹所在的根目录当前设为C盘根目录需根据实际改
E_DISK_FOLDERS_ROOT_DIR = "E:\\"
# 日志文件配置保存在F盘与脚本同级
PASS_LOG_FILE = "passed_files.txt" # 通过视频日志
FAIL_LOG_FILE = "rejected_files.txt" # 不通过视频日志
# 新增视频检测间隔(秒)
DETECTION_INTERVAL = 5 # 每5秒检测一次新视频
# =================================================================
class VideoProcessorGUI:
def __init__(self, root):
"""初始化GUI窗口和变量"""
self.root = root
self.root.title("视频批量标记工具 (支持实时新增视频)")
self.root.geometry("550x220")
self.root.resizable(False, False)
# 检查E盘当前是C盘文件夹根目录是否存在
if not os.path.isdir(E_DISK_FOLDERS_ROOT_DIR):
messagebox.showerror("错误", f"目标盘文件夹根目录不存在!\n请检查路径: {E_DISK_FOLDERS_ROOT_DIR}")
self.root.destroy()
return
# 初始化视频文件列表(存储完整路径)
self.video_files = self._get_video_files()
self.current_index = 0
self.last_checked_time = datetime.now() # 记录最后一次检测时间
# 创建界面组件
self._create_widgets()
# 开始处理第一个视频
self._process_current_video()
# 启动新视频检测定时器
self._start_detection_timer()
def _force_focus_timer(self):
"""定时强制获取焦点"""
self.root.focus_force() # 强制获取焦点
self.root.after(500, self._force_focus_timer)
def _get_video_files(self):
"""获取F盘当前目录下的所有视频文件仅返回完整路径"""
video_extensions = ('.mp4', '.mkv', '.avi', '.mov', '.flv', '.wmv', '.webm', '.ts')
video_files = [
os.path.join(F_DISK_VIDEO_DIR, f)
for f in os.listdir(F_DISK_VIDEO_DIR)
if os.path.isfile(os.path.join(F_DISK_VIDEO_DIR, f)) and f.lower().endswith(video_extensions)
]
return video_files # 不排序,确保新增文件按系统顺序添加
def _create_widgets(self):
"""创建弹窗中的按钮、文字等组件"""
# 新视频提示标签(红色提示新增文件)
self.new_files_label = tk.Label(self.root, text="", fg="red", pady=5)
self.new_files_label.pack()
# 状态提示如“正在处理第1/5个”
self.status_label = tk.Label(self.root, text="", pady=5)
self.status_label.pack()
# 当前视频文件名(蓝色加粗)
self.filename_label = tk.Label(self.root, text="", font=("Arial", 12), fg="blue")
self.filename_label.pack()
# 按钮容器(让按钮横向排列)
button_frame = tk.Frame(self.root)
button_frame.pack(side=tk.BOTTOM, pady=20)
self.root.bind('<Key>', self._handle_key_press)
self.root.focus_force() # 强制获取焦点
self.root.bind('<FocusOut>', lambda e: self.root.focus_force())
# 上一条/重播/通过/不通过/下一条按钮
self.prev_btn = tk.Button(button_frame, text="上一条", width=10, command=self._prev_video)
self.replay_btn = tk.Button(button_frame, text="重播", width=10, command=self._replay_video)
self.pass_btn = tk.Button(button_frame, text="通过", width=10, command=self._mark_as_pass, bg="green", fg="white")
self.fail_btn = tk.Button(button_frame, text="不通过", width=10, command=self._mark_as_fail, bg="red", fg="white")
self.next_btn = tk.Button(button_frame, text="下一条", width=10, command=self._next_video)
# 按钮布局(横向均匀分布)
self.prev_btn.pack(side=tk.LEFT, padx=5)
self.replay_btn.pack(side=tk.LEFT, padx=5)
self.pass_btn.pack(side=tk.LEFT, padx=5)
self.fail_btn.pack(side=tk.LEFT, padx=5)
self.next_btn.pack(side=tk.LEFT, padx=5)
def _start_detection_timer(self):
"""启动定时器,定期检测新视频文件"""
self._check_new_videos() # 立即执行一次检测
# 每隔DETECTION_INTERVAL秒重复检测
self.root.after(DETECTION_INTERVAL * 1000, self._start_detection_timer)
def _check_new_videos(self):
"""检测并添加新增的视频文件,若之前无视频则自动启动播放"""
if not os.path.isdir(F_DISK_VIDEO_DIR):
return # 目录不存在则跳过检测
# 获取当前目录下的所有视频(最新状态)
current_all_videos = self._get_video_files()
# 找出不在当前处理列表中的新视频(通过路径对比)
existing_paths = set(self.video_files)
new_videos = [path for path in current_all_videos if path not in existing_paths]
if new_videos:
# 记录新增前的列表长度(判断是否之前无视频)
was_empty = len(self.video_files) == 0
# 将新视频添加到处理列表末尾
self.video_files.extend(new_videos)
# 提取新视频的文件名(用于提示)
new_filenames = [os.path.basename(path) for path in new_videos]
# 显示新增提示3秒后自动清除提示
self.new_files_label.config(text=f"检测到 {len(new_videos)} 个新视频,已添加到列表:{', '.join(new_filenames[:2])}{'...' if len(new_filenames)>2 else ''}")
self.root.after(3000, lambda: self.new_files_label.config(text="")) # 3秒后清除提示
print(f"新增视频:{new_filenames}")
# 核心修复:若之前无视频,新增后自动从第一个新视频开始播放
if was_empty:
self.current_index = 0 # 确保从第一个新增视频开始
self._process_current_video() # 触发播放
# 更新最后检测时间
self.last_checked_time = datetime.now()
def _play_video(self, video_path):
"""用系统默认播放器打开视频"""
try:
if sys.platform == 'win32': # 针对Windows系统
os.startfile(video_path)
else: # 针对Mac/Linux系统备用
opener = 'open' if sys.platform == 'darwin' else 'xdg-open'
subprocess.run([opener, video_path], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except Exception as e:
# 播放失败时弹窗提示
error_msg = (
f"无法播放视频!\n\n"
f"系统错误信息: {e}\n\n"
f"**请检查:**\n"
f"1. 您是否已安装视频播放器?\n"
f"2. 您是否已将其设置为默认播放器?"
)
messagebox.showerror("播放错误", error_msg)
def _log_file(self, filename, log_type):
"""通用日志记录方法根据log_type记录到对应的TXT文件"""
try:
# 选择日志文件通过→passed_files.txt不通过→rejected_files.txt
log_file = PASS_LOG_FILE if log_type == "pass" else FAIL_LOG_FILE
# 日志文件路径和脚本、视频在同一目录F盘
log_file_path = os.path.join(F_DISK_VIDEO_DIR, log_file)
# 追加写入(不覆盖历史记录),包含时间戳
with open(log_file_path, 'a', encoding='utf-8') as f:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
f.write(f"[{timestamp}] {filename}\n")
return True # 记录成功
except Exception as e:
print(f"写入日志失败: {e}") # 终端打印错误(不影响用户操作)
return False # 记录失败
def _handle_key_press(self, event):
"""处理键盘按键"""
if event.char == '2': # 按下1键
self._mark_as_pass()
elif event.char == '3': # 按下2键
self._mark_as_fail()
def _process_current_video(self):
"""更新弹窗状态,并自动播放当前视频"""
if not self.video_files: # 没有视频可处理时
self.status_label.config(text="所有视频处理完毕!(将继续检测新视频...", fg="green")
self.filename_label.config(text="")
return
# 获取当前视频路径和文件名
current_video_path = self.video_files[self.current_index]
current_video_name = os.path.basename(current_video_path)
# 更新弹窗文字提示(覆盖之前的“处理完毕”提示)
self.status_label.config(text=f"正在处理: 第 {self.current_index + 1}/{len(self.video_files)}")
self.filename_label.config(text=current_video_name)
# 自动播放当前视频
self._play_video(current_video_path)
def _get_associated_folder(self, video_path):
"""获取E盘当前是C盘上与视频同名的文件夹路径仅查询不操作"""
video_basename = os.path.basename(video_path)
folder_name = os.path.splitext(video_basename)[0] # 提取视频名(去后缀)
folder_path = os.path.join(E_DISK_FOLDERS_ROOT_DIR, folder_name)
if os.path.isdir(folder_path):
return folder_path
return None
def _mark_as_pass(self):
"""处理“通过”:删除视频+记录到passed_files.txt"""
if not self.video_files:
return
os.system('taskkill /f /im "vlc.exe"')
# 获取当前视频信息(路径、带后缀名、去后缀名)
current_video_path = self.video_files[self.current_index]
video_basename = os.path.basename(current_video_path)
video_filename = os.path.splitext(video_basename)[0] # 去后缀的文件名如“视频1”
# 1. 记录“通过”日志
log_success = self._log_file(video_filename, "pass")
# 2. 提示用户操作结果(包含日志记录情况)
if log_success:
messagebox.showinfo("操作结果",
f"已标记为通过: {video_filename}\n"
f"文件名已记录到 {PASS_LOG_FILE}")
else:
messagebox.showerror("操作结果",
f"已标记为通过: {video_filename}\n"
f"警告:文件名未成功记录到 {PASS_LOG_FILE}")
# 3. 删除F盘的视频移至回收站安全删除
try:
send2trash(current_video_path)
print(f"通过视频已删除: {current_video_path}")
except Exception as e:
messagebox.showerror("操作失败",
f"无法删除视频: {e}\n"
f"请确保视频没有被播放器占用(关闭播放器后重试)")
# 4. 处理下一个视频
self._move_to_next()
def _mark_as_fail(self):
"""处理“不通过”:删除视频+记录到rejected_files.txt"""
if not self.video_files:
return
os.system('taskkill /f /im "vlc.exe"')
# 获取当前视频信息
current_video_path = self.video_files[self.current_index]
video_basename = os.path.basename(current_video_path)
video_filename = os.path.splitext(video_basename)[0]
# 1. 记录“不通过”日志
log_success = self._log_file(video_filename, "fail")
# 2. 提示用户操作结果
if log_success:
messagebox.showinfo("操作结果",
f"已标记为不通过: {video_filename}\n"
f"文件名已记录到 {FAIL_LOG_FILE}")
else:
messagebox.showerror("操作结果",
f"已标记为不通过: {video_filename}\n"
f"警告:文件名未成功记录到 {FAIL_LOG_FILE}")
# 3. 删除F盘的视频移至回收站
try:
send2trash(current_video_path)
print(f"不通过视频已删除: {current_video_path}")
except Exception as e:
messagebox.showerror("操作失败",
f"无法删除视频: {e}\n"
f"请确保视频没有被播放器占用(关闭播放器后重试)")
# 4. 处理下一个视频
self._move_to_next()
def _move_to_next(self):
"""移除已处理的视频,切换到下一个"""
if not self.video_files:
return
self.video_files.pop(self.current_index)
# 避免删除最后一个视频后索引越界
if self.current_index >= len(self.video_files) and self.video_files:
self.current_index = len(self.video_files) - 1
# 更新弹窗状态,播放下一个视频
self._process_current_video()
def _replay_video(self):
"""重播当前视频"""
if self.video_files:
self._play_video(self.video_files[self.current_index])
def _next_video(self):
"""切换到下一个视频(不处理当前视频)"""
if not self.video_files:
return
if self.current_index < len(self.video_files) - 1:
self.current_index += 1
self._process_current_video()
else:
messagebox.showinfo("提示", "已经是最后一个视频了。")
def _prev_video(self):
"""切换到上一个视频(不处理当前视频)"""
if not self.video_files:
return
if self.current_index > 0:
self.current_index -= 1
self._process_current_video()
else:
messagebox.showinfo("提示", "已经是第一个视频了。")
if __name__ == "__main__":
# 启动弹窗程序
root = tk.Tk()
app = VideoProcessorGUI(root)
root.mainloop()

177
x时间后按下x按键.py Normal file
View File

@ -0,0 +1,177 @@
import time
import threading
from pynput import keyboard
from pynput.keyboard import Key, Controller
class AutoKeyPresser:
# =============== 全局配置 ===============
CONTROL_KEY = 'p' # 控制定时器启动/暂停的快捷键
TARGET_KEY = Key.f10 # 要自动按下的目标键默认为F10
INTERVAL = 60 + 52 # 定时器间隔时间1分钟52秒 = 112秒
# =============== 结束配置 ===============
def __init__(self):
self.timer = None
self.is_active = False
self.listener = None
self.keyboard_controller = Controller()
self.ignore_next_key = False # 标志位,用于忽略程序自动按下的目标键
self.timer_lock = threading.Lock() # 线程锁确保状态同步
self.last_activity_time = 0 # 记录最后一次活动时间
def simulate_key_press(self):
"""模拟按下配置的目标键"""
try:
self.ignore_next_key = True # 设置标志,忽略接下来的目标键事件
# 使用正确的键模拟
self.keyboard_controller.press(self.TARGET_KEY)
time.sleep(0.05) # 短暂延迟模拟真实按键
self.keyboard_controller.release(self.TARGET_KEY)
current_time = time.strftime("%Y-%m-%d %H:%M:%S")
key_name = self._get_key_name(self.TARGET_KEY)
print(f"[{current_time}] 已自动按下 {key_name}")
except Exception as e:
print(f"模拟按下 {self._get_key_name(self.TARGET_KEY)} 时发生错误: {e}")
def _get_key_name(self, key):
"""获取按键的可读名称"""
if hasattr(key, 'name'):
return key.name.upper()
elif hasattr(key, 'char'):
return key.char.upper()
return str(key)
def start_timer(self):
"""启动定时器总是重新开始112秒计时"""
with self.timer_lock:
if self.is_active:
return
self.is_active = True
self.last_activity_time = time.time() # 重置开始时间
current_time = time.strftime("%Y-%m-%d %H:%M:%S")
key_name = self._get_key_name(self.TARGET_KEY)
print(f"[{current_time}] 定时器已启动,将在{self.INTERVAL}秒后按下 {key_name}")
print(f"{self.CONTROL_KEY.upper()} 键可以暂停定时器")
def timer_loop():
# 记录本次循环的开始时间
cycle_start_time = time.time()
while self.is_active:
# 计算已经过去的时间
elapsed_time = time.time() - cycle_start_time
remaining_time = self.INTERVAL - elapsed_time
# 如果已经达到或超过间隔时间,执行按键操作
if remaining_time <= 0:
if self.is_active: # 再次检查状态
self.simulate_key_press()
# 重置循环开始时间,开始新的周期
cycle_start_time = time.time()
continue
# 分段等待,便于及时响应暂停命令
wait_interval = min(0.5, remaining_time) # 每次最多等待0.5秒
if wait_interval > 0:
time.sleep(wait_interval)
self.timer = threading.Thread(target=timer_loop)
self.timer.daemon = True
self.timer.start()
def pause_timer(self):
"""暂停定时器"""
with self.timer_lock:
if not self.is_active:
return
self.is_active = False
current_time = time.strftime("%Y-%m-%d %H:%M:%S")
# 计算已过去的时间(用于显示信息)
if self.last_activity_time > 0:
elapsed = time.time() - self.last_activity_time
remaining = max(0, self.INTERVAL - elapsed)
print(f"[{current_time}] 定时器已暂停(已等待{elapsed:.1f}秒,剩余{remaining:.1f}秒)")
else:
print(f"[{current_time}] 定时器已暂停")
def on_press(self, key):
"""键盘按键事件处理"""
try:
# 检测控制键按下,用于控制定时器暂停/恢复
if hasattr(key, 'char') and (key.char.lower() == self.CONTROL_KEY.lower()):
if not self.is_active:
# 按下控制键,启动定时器(重新开始计时)
self.start_timer()
else:
# 定时器运行中按下控制键,暂停定时器
self.pause_timer()
# 处理Esc键事件 - 不再退出程序,只显示信息
elif key == Key.esc:
current_time = time.strftime("%Y-%m-%d %H:%M:%S")
status = '运行中' if self.is_active else '已暂停'
print(f"[{current_time}] 检测到按下Esc键定时器状态{status}")
print("提示Esc键已禁用退出功能程序继续运行")
# 处理目标键事件(仅用于显示,不影响定时器状态)
elif key == self.TARGET_KEY:
if self.ignore_next_key:
# 如果是程序自动按下的键,忽略它
self.ignore_next_key = False
else:
# 用户手动按下的目标键,只显示信息不影响定时器
current_time = time.strftime("%Y-%m-%d %H:%M:%S")
status = '运行中' if self.is_active else '已暂停'
key_name = self._get_key_name(self.TARGET_KEY)
print(f"[{current_time}] 检测到手动按下 {key_name} 键(定时器状态:{status}")
except AttributeError:
# 处理特殊功能键没有char属性的键
if key == Key.esc:
current_time = time.strftime("%Y-%m-%d %H:%M:%S")
status = '运行中' if self.is_active else '已暂停'
print(f"[{current_time}] 检测到按下Esc键定时器状态{status}")
print("提示Esc键已禁用退出功能程序继续运行")
elif key == self.TARGET_KEY:
if self.ignore_next_key:
self.ignore_next_key = False
else:
current_time = time.strftime("%Y-%m-%d %H:%M:%S")
status = '运行中' if self.is_active else '已暂停'
key_name = self._get_key_name(self.TARGET_KEY)
print(f"[{current_time}] 检测到手动按下 {key_name} 键(定时器状态:{status}")
def on_release(self, key):
"""键盘释放事件处理 - 移除Esc键退出功能"""
pass
def start_listening(self):
"""开始监听键盘事件"""
key_name = self._get_key_name(self.TARGET_KEY)
print(f"{key_name}自动按下程序已启动")
print(f"{self.CONTROL_KEY.upper()} 键开始定时器,再次按 {self.CONTROL_KEY.upper()} 键暂停定时器")
print(f"定时器间隔时间:{self.INTERVAL}")
print(f"目标按键:{key_name}")
print("提示:定时器暂停后重新启动将重新开始计时")
print("提示Esc键退出功能已禁用如需退出程序请使用Ctrl+C")
# 同时监听按下和释放事件
with keyboard.Listener(on_press=self.on_press, on_release=self.on_release) as self.listener:
try:
self.listener.join()
except Exception as e:
print(f"监听器错误: {e}")
if __name__ == "__main__":
try:
auto_presser = AutoKeyPresser()
auto_presser.start_listening()
except KeyboardInterrupt:
print("\n程序被用户中断Ctrl+C")
except Exception as e:
print(f"程序运行错误: {e}")

44
一直按着x按键.py Normal file
View File

@ -0,0 +1,44 @@
import keyboard
import time
import sys
# 可配置参数
START_STOP_KEY = '-' # 启动/停止按键
HOLD_KEY = 'w' # 程序运行时持续按下的键
EXIT_KEY = 'esc' # 注意虽然你要求ESC不退出但我们会忽略它
# 状态变量
is_running = False
should_exit = False
def on_p_pressed(e):
global is_running
if e.event_type == keyboard.KEY_DOWN:
is_running = not is_running
if not is_running: # 当停止时立即释放按键
keyboard.release(HOLD_KEY)
print(f"程序 {'已启动' if is_running else '已停止'}")
def main():
print(f"程序已启动,按 {START_STOP_KEY} 键开始/停止,按 Ctrl+C 退出")
print(f"程序运行时将持续按下 {HOLD_KEY}")
keyboard.hook_key(START_STOP_KEY, on_p_pressed)
try:
while not should_exit:
if is_running:
keyboard.press(HOLD_KEY)
# 保持按键状态,但允许其他事件处理
time.sleep(0.1)
else:
time.sleep(0.1)
except KeyboardInterrupt:
print("\n程序通过 Ctrl+C 退出")
finally:
if is_running:
keyboard.release(HOLD_KEY)
keyboard.unhook_all()
if __name__ == "__main__":
main()

389
可视化质检.py Normal file
View File

@ -0,0 +1,389 @@
import os
import tkinter as tk
from tkinter import messagebox
import subprocess
import sys
import time
# =============== 全局命令配置 ===============
COMMANDS_TEMPLATE = [
"conda activate aiden-p3d",
"d:",
"cd aiden-p3d",
]
# 扫描路径配置默认F盘根目录
SCAN_PATH = "E:\\" # 修改为您需要的默认扫描路径
# 新增文件夹检测间隔(秒)
DETECTION_INTERVAL = 5 # 每5秒检测一次新文件夹
# 游戏配置映射
GAME_COMMANDS = {
"赛博朋克2077": {
"command": "python load_point_cloud_saibo_new.py \"{}\"",
"description": "赛博朋克2077 - python load_point_cloud_saibo_new.py"
},
"巫师3": {
"command": "python load_point_cloud_wushi_new.py \"{}\"",
"description": "巫师3 - python load_point_cloud_wushi_new.py"
},
"孤岛危机": {
"command": "python load_point_cloud_crysis_new.py \"{}\"",
"description": "孤岛危机 - python load_point_cloud_crysis_new.py"
},
"地平线零之曙光": {
"command": "python load_point_horizon_new.py \"{}\"",
"description": "地平线零之曙光 - python load_point_horizon_new.py"
},
"原子之心": {
"command": "python debug_atomicHeart.py -d \"{}\"",
"description": "原子之心 - python debug_atomicHeart.py -d"
},
"巴士模拟18": {
"command": "python debug_bus18.py -d \"{}\"",
"description": "巴士模拟18 - python debug_bus18.py -d"
},
"超自然车旅": {
"command": "python debug_pacificdrive.py -d \"{}\"",
"description": "超自然车旅 - python debug_pacificdrive.py -d"
},
"印蒂卡": {
"command": "python debug_indika.py -d \"{}\"",
"description": "印蒂卡 - python debug_indika.py -d"
},
"黑神话:悟空": {
"command": "python restruct_b1.py --data-dir \"{}\"",
"description": "黑神话:悟空 - python restruct_b1.py --data-dir"
},
"新游戏选项": {
"command": "conda activate worldgen && E: && cd datacheck && run.bat \"{}\"",
"description": "新游戏选项 - E: && cd datacheck && run.bat"
}
}
# MeshLab路径
MESHLAB_PATH = r"C:\Program Files\VCG\MeshLab\meshlab.exe"
# 输出目录
OUTPUT_DIR = r"E:\datacheck\outputs"
# =============== 结束配置 ===============
class FolderPlayerApp:
def __init__(self, root):
self.root = root
self.root.title("CV Saved Folders Player")
self.root.geometry("700x250") # 增加窗口宽度以适应新按钮
self.root.resizable(False, False)
self.selected_game = tk.StringVar(value="赛博朋克2077")
self.scan_folders()
if not self.folders:
messagebox.showerror("错误", f"在路径 {SCAN_PATH} 中没有找到以'cv_saved_''25'为前缀的文件夹")
sys.exit(1)
self.current_index = 0
self.process = None
self.last_scan_time = time.time()
self.create_ui()
self.update_display()
self.start_detection_timer()
self._force_focus_timer()
def _force_focus_timer(self):
self.root.focus_force()
self.root.after(500, self._force_focus_timer)
def start_detection_timer(self):
self.check_new_folders()
self.root.after(DETECTION_INTERVAL * 1000, self.start_detection_timer)
def check_new_folders(self):
current_dir = SCAN_PATH
try:
all_items = os.listdir(current_dir)
except Exception as e:
print(f"检测新文件夹时出错: {e}")
return
processed_folders = set()
for filename in ["passed_files.txt", "rejected_files.txt"]:
if os.path.exists(filename):
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
processed_folders.add(line)
new_folders = [
os.path.join(current_dir, item)
for item in all_items
if ((item.startswith("cv_saved_") or item.startswith("25")) and
os.path.isdir(os.path.join(current_dir, item)) and
item not in processed_folders and
os.path.join(current_dir, item) not in self.folders)
]
if new_folders:
new_folders.sort()
was_empty = len(self.folders) == 0
self.folders.extend(new_folders)
if was_empty and self.folders:
self.current_index = 0
self.update_display()
messagebox.showinfo("提示", f"检测到 {len(new_folders)} 个新文件夹,已添加到列表")
def scan_folders(self):
current_dir = SCAN_PATH
try:
all_items = os.listdir(current_dir)
except FileNotFoundError:
messagebox.showerror("错误", f"指定的扫描路径不存在: {SCAN_PATH}")
sys.exit(1)
except PermissionError:
messagebox.showerror("错误", f"没有权限访问指定路径: {SCAN_PATH}")
sys.exit(1)
processed_folders = set()
for filename in ["passed_files.txt", "rejected_files.txt"]:
if os.path.exists(filename):
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
processed_folders.add(line)
self.folders = [
os.path.join(current_dir, item)
for item in all_items
if ((item.startswith("cv_saved_") or item.startswith("25")) and
os.path.isdir(os.path.join(current_dir, item)) and
item not in processed_folders)
]
self.folders.sort()
def create_ui(self):
game_frame = tk.Frame(self.root)
game_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(game_frame, text="选择游戏:", font=("Arial", 10)).pack(side=tk.LEFT)
game_dropdown = tk.OptionMenu(game_frame, self.selected_game, *GAME_COMMANDS.keys())
game_dropdown.pack(side=tk.LEFT, padx=5)
self.status_label = tk.Label(self.root, text="", pady=5)
self.status_label.pack()
self.folder_label = tk.Label(self.root, text="", font=("Arial", 12), fg="blue")
self.folder_label.pack()
button_frame = tk.Frame(self.root)
button_frame.pack(side=tk.BOTTOM, pady=20)
self.prev_button = tk.Button(
button_frame,
text="上一条(<-)",
width=10,
command=self.prev_folder,
state=tk.DISABLED if self.current_index == 0 else tk.NORMAL
)
self.prev_button.pack(side=tk.LEFT, padx=5)
self.play_button = tk.Button(
button_frame,
text="播放(空格)",
width=10,
command=self.play_current
)
self.play_button.pack(side=tk.LEFT, padx=5)
self.pass_button = tk.Button(
button_frame,
text="通过(p)",
width=10,
command=self.mark_as_passed,
bg="green",
fg="white"
)
self.pass_button.pack(side=tk.LEFT, padx=5)
self.reject_button = tk.Button(
button_frame,
text="不通过(r)",
width=10,
command=self.mark_as_rejected,
bg="red",
fg="white"
)
self.reject_button.pack(side=tk.LEFT, padx=5)
self.next_button = tk.Button(
button_frame,
text="下一条(->)",
width=10,
command=self.next_folder,
state=tk.DISABLED if self.current_index == len(self.folders) - 1 else tk.NORMAL
)
self.next_button.pack(side=tk.LEFT, padx=5)
# 新增打开pts.ply文件按钮
self.open_ply_button = tk.Button(
button_frame,
text="打开PLY(o)",
width=10,
command=self.open_ply_file,
bg="orange",
fg="white"
)
self.open_ply_button.pack(side=tk.LEFT, padx=5)
self.root.bind('<Left>', lambda event: self.prev_folder())
self.root.bind('<Right>', lambda event: self.next_folder())
self.root.bind('<space>', lambda event: self.play_current())
self.root.bind('<p>', lambda event: self.mark_as_passed())
self.root.bind('<r>', lambda event: self.mark_as_rejected())
self.root.bind('<o>', lambda event: self.open_ply_file()) # 绑定o键
def update_display(self):
if not self.folders:
self.status_label.config(text="所有文件夹已处理完毕")
self.folder_label.config(text="")
return
current_path = self.folders[self.current_index]
folder_name = os.path.basename(current_path)
self.status_label.config(text=f"正在处理: 第 {self.current_index + 1}/{len(self.folders)}")
self.folder_label.config(text=folder_name)
self.prev_button.config(state=tk.NORMAL if self.current_index > 0 else tk.DISABLED)
self.next_button.config(state=tk.NORMAL if self.current_index < len(self.folders) - 1 else tk.DISABLED)
def prev_folder(self):
if self.current_index > 0:
self.current_index -= 1
self.update_display()
def next_folder(self):
if self.current_index < len(self.folders) - 1:
self.current_index += 1
self.update_display()
def play_current(self):
if not self.folders:
return
current_path = self.folders[self.current_index]
selected_game_name = self.selected_game.get()
game_config = GAME_COMMANDS.get(selected_game_name)
if not game_config:
messagebox.showerror("错误", f"未找到游戏 {selected_game_name} 的配置")
return
try:
self.close_command_prompt()
time.sleep(0.5)
if selected_game_name == "新游戏选项":
# 执行新游戏选项命令不使用COMMANDS_TEMPLATE
game_command = game_config["command"]
formatted_command = game_command.format(current_path)
# 启动命令(不等待)
self.process = subprocess.Popen(f'start cmd /k "{formatted_command}"', shell=True)
else:
# 其他游戏使用原有的COMMANDS_TEMPLATE
game_command = game_config["command"]
formatted_command = game_command.format(current_path)
commands = COMMANDS_TEMPLATE.copy()
commands.append(formatted_command)
full_command = " && ".join(commands)
self.process = subprocess.Popen(f'start cmd /k "{full_command}"', shell=True)
except Exception as e:
messagebox.showerror("错误", f"执行命令时出错:\n{str(e)}")
def open_ply_file(self):
"""打开output目录中的pts.ply文件"""
ply_file = os.path.join(OUTPUT_DIR, "pts.ply")
if os.path.exists(ply_file):
try:
subprocess.Popen([MESHLAB_PATH, ply_file])
messagebox.showinfo("成功", f"已用MeshLab打开文件: {ply_file}")
except Exception as e:
messagebox.showerror("错误", f"打开文件时出错:\n{str(e)}")
else:
messagebox.showwarning("警告", f"未找到文件: {ply_file}")
def close_command_prompt(self):
try:
if self.process:
self.process.terminate()
subprocess.Popen('taskkill /f /im cmd.exe', shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW)
except Exception as e:
print(f"关闭命令行时出错: {e}")
def mark_as_passed(self):
self._mark_file("passed_files.txt")
def mark_as_rejected(self):
self._mark_file("rejected_files.txt")
def _mark_file(self, filename):
if not self.folders:
return
current_path = self.folders[self.current_index]
folder_name = os.path.basename(current_path)
try:
if not os.path.exists(filename):
with open(filename, 'w', encoding='utf-8') as f:
pass
with open(filename, 'a', encoding='utf-8') as f:
if os.path.getsize(filename) == 0:
f.write(f"{folder_name}\n")
else:
needs_newline = True
with open(filename, 'rb+') as f_check:
f_check.seek(-1, os.SEEK_END)
last_char = f_check.read(1)
needs_newline = (last_char != b'\n')
if needs_newline:
f.write('\n')
f.write(f"{folder_name}\n")
status = "通过" if filename == "passed_files.txt" else "不通过"
messagebox.showinfo("成功", f"已标记为{status}: {folder_name}")
self.folders.pop(self.current_index)
if self.current_index >= len(self.folders):
if len(self.folders) > 0:
self.current_index = len(self.folders) - 1
else:
messagebox.showinfo("完成", "所有文件夹已处理完毕!")
self.update_display()
return
self.update_display()
except Exception as e:
messagebox.showerror("错误", f"写入文件时出错:\n{str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = FolderPlayerApp(root)
root.mainloop()

84
战神修改文件名.py Normal file
View File

@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
fix_commas_and_pad_multi.py
批量修复多个文件夹中含逗号的数字文件名并补齐到6位例如:
"01,000.png" -> "001000.png"
支持扫描指定文件夹中所有以"25"开头的子文件夹
"""
import os
import argparse
VALID_EXTS = {'.png', '.exr'}
def safe_rename(src, dst, dry_run=False):
if os.path.exists(dst):
print(f"[跳过] 目标已存在,未重命名:{os.path.basename(src)} -> {os.path.basename(dst)}")
return False
print(f"[重命名] {os.path.basename(src)} -> {os.path.basename(dst)}")
if not dry_run:
os.rename(src, dst)
return True
def fix_folder(root_folder, dry_run=False):
changed = 0
for root, dirs, files in os.walk(root_folder):
for fname in files:
name, ext = os.path.splitext(fname)
if ext.lower() not in VALID_EXTS:
continue
# 若文件名中含逗号,则移除逗号
if ',' in name:
candidate = name.replace(',', '')
else:
candidate = None
if candidate is None or not candidate.isdigit():
continue
padded = candidate.zfill(6)
if padded == name:
continue
src_path = os.path.join(root, fname)
dst_path = os.path.join(root, padded + ext)
if safe_rename(src_path, dst_path, dry_run=dry_run):
changed += 1
print(f"[完成] {root_folder} 共重命名 {changed} 个文件。\n")
def find_25_folders(root_folder):
"""查找所有以'25'开头的子文件夹"""
folders = []
for item in os.listdir(root_folder):
item_path = os.path.join(root_folder, item)
if os.path.isdir(item_path) and item.startswith('25'):
folders.append(item_path)
return folders
def main():
p = argparse.ArgumentParser(
description="修复含逗号的数字文件名并补齐到6位例如 01,000.png -> 001000.png扫描指定文件夹中所有以25开头的子文件夹"
)
p.add_argument("root_folder", help="包含多个以25开头的子文件夹的根目录")
p.add_argument("--dry-run", action="store_true", help="仅模拟显示将要做的重命名,不执行")
args = p.parse_args()
if not os.path.isdir(args.root_folder):
print(f"❌ 路径无效:{args.root_folder}")
return
target_folders = find_25_folders(args.root_folder)
if not target_folders:
print(f"❌ 在 {args.root_folder} 中未找到以'25'开头的子文件夹")
return
print(f"\n=== 在 {args.root_folder} 中找到 {len(target_folders)} 个以'25'开头的子文件夹 ===")
for folder in target_folders:
print(f"\n=== 处理文件夹:{folder} ===")
fix_folder(folder, dry_run=args.dry_run)
if __name__ == "__main__":
main()

61
脚本管理器.py Normal file
View File

@ -0,0 +1,61 @@
import os
import subprocess
from PyQt5.QtWidgets import (QApplication, QMainWindow, QListWidget,
QVBoxLayout, QLineEdit, QMessageBox, QWidget)
from PyQt5.QtCore import Qt
class ScriptRunner(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Python脚本启动器")
self.setGeometry(100, 100, 600, 400)
self.script_dir = r"D:\test\集合"
self.init_ui()
self.load_scripts()
def init_ui(self):
central = QWidget()
layout = QVBoxLayout(central)
self.search_box = QLineEdit()
self.search_box.setPlaceholderText("搜索脚本...")
self.search_box.textChanged.connect(self.filter_scripts)
layout.addWidget(self.search_box)
self.script_list = QListWidget()
self.script_list.itemDoubleClicked.connect(self.run_script)
layout.addWidget(self.script_list)
self.setCentralWidget(central)
def load_scripts(self):
self.script_list.clear()
if os.path.exists(self.script_dir):
for f in os.listdir(self.script_dir):
if f.endswith('.py'):
self.script_list.addItem(f)
else:
QMessageBox.warning(self, "错误", f"目录不存在: {self.script_dir}")
def filter_scripts(self):
keyword = self.search_box.text().lower()
for i in range(self.script_list.count()):
item = self.script_list.item(i)
item.setHidden(keyword not in item.text().lower())
def run_script(self, item):
script_name = item.text()
script_path = os.path.join(self.script_dir, script_name)
try:
# 使用subprocess在新窗口中运行
subprocess.Popen(f'start cmd /k python "{script_path}"', shell=True)
self.statusBar().showMessage(f"正在运行: {script_name}")
except Exception as e:
QMessageBox.critical(self, "错误", f"运行失败: {str(e)}")
if __name__ == "__main__":
app = QApplication([])
window = ScriptRunner()
window.show()
app.exec_()

167
转移文件non.py Normal file
View File

@ -0,0 +1,167 @@
import os
import time
import shutil
import subprocess
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import concurrent.futures
import threading
# ====== 配置 ======
SOURCE_FOLDER = Path(r"E:\ELDEN RING\Game\cv_saved") # 监控的源目录
DEST_FOLDER = Path(r"E:") # 目标目录(必须是具体路径)
FOLDER_PREFIX = "25" # 需要监控的文件夹前缀
MAX_WORKERS = 8 # 最大线程数
STABLE_TIME = 30 # 文件夹稳定检查时间(秒)
# =================
def is_folder_stable(folder_path):
"""检查文件夹是否稳定(连续两次修改时间相同)"""
try:
initial_mtime = os.path.getmtime(folder_path)
time.sleep(STABLE_TIME)
current_mtime = os.path.getmtime(folder_path)
return initial_mtime == current_mtime
except Exception as e:
print(f" [错误] 检查稳定性失败: {e}")
return False
def process_folder(folder_path):
"""使用 robocopy 移动文件夹"""
start_time = time.time()
folder_name = folder_path.name
print(f"\n[处理] 开始移动文件夹: '{folder_name}'")
print(f" 源路径: {folder_path}")
print(f" 目标路径: {DEST_FOLDER / folder_name}")
try:
# 检查目标目录权限
if not os.access(DEST_FOLDER, os.W_OK):
raise PermissionError(f"无写入权限: {DEST_FOLDER}")
# 确保目标目录存在
DEST_FOLDER.mkdir(parents=True, exist_ok=True)
# 构造 robocopy 命令
cmd = [
"robocopy",
str(folder_path), # 源目录
str(DEST_FOLDER / folder_name), # 目标目录
"/MIR", # 镜像目录树
"/MT:8", # 多线程8线程
"/R:2", # 失败重试2次
"/W:5", # 重试等待5秒
"/NFL", "/NDL", "/NJH", "/NJS" # 减少日志输出
]
print(f" 执行命令: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
# 处理 robocopy 返回码
if result.returncode >= 8:
raise RuntimeError(f"Robocopy 失败 (代码 {result.returncode}): {result.stderr}")
# 删除源目录robocopy /MIR 已处理,此处作为二次确认)
if (DEST_FOLDER / folder_name).exists():
print(f" 验证: 目标文件夹已存在,正在清理源文件夹...")
shutil.rmtree(folder_path, ignore_errors=True)
print(f" [完成] '{folder_name}' 移动成功")
else:
raise FileNotFoundError("目标文件夹未创建,终止删除源文件夹")
except Exception as e:
print(f" [错误] 处理失败: {e}")
finally:
duration = time.time() - start_time
print(f" 耗时: {duration:.2f}")
class FolderMonitor(FileSystemEventHandler):
def __init__(self, executor):
super().__init__()
self.executor = executor
self.active_tasks = set() # 跟踪正在处理的任务
def on_created(self, event):
"""处理新创建的文件夹"""
self._handle_folder(event.src_path, "创建")
def on_moved(self, event):
"""处理移动/重命名的文件夹"""
self._handle_folder(event.dest_path, "移动")
def _handle_folder(self, path, action_type):
"""统一处理文件夹事件"""
folder_path = Path(path)
if not folder_path.is_dir():
return
if folder_path.parent != SOURCE_FOLDER or not folder_path.name.startswith(FOLDER_PREFIX):
return
print(f"\n[发现] 通过 {action_type} 事件检测到文件夹: '{folder_path.name}'")
# 避免重复处理
if folder_path.name in self.active_tasks:
print(f" 警告: 文件夹 '{folder_path.name}' 已在处理中")
return
self.active_tasks.add(folder_path.name)
self.executor.submit(self._process_with_stability_check, folder_path)
def _process_with_stability_check(self, folder_path):
"""带稳定性检查的处理流程(无限等待直到稳定)"""
folder_name = folder_path.name
try:
while True:
if is_folder_stable(folder_path):
print(f" [稳定] 文件夹 '{folder_name}' 已稳定,开始移动")
process_folder(folder_path)
break
else:
print(f" [监控] 文件夹 '{folder_name}' 仍在修改中,继续等待...")
except Exception as e:
print(f" [错误] 处理 '{folder_name}' 时发生异常: {e}")
finally:
self.active_tasks.discard(folder_name)
def main():
print("\n===== 文件夹监控转移程序 =====")
print(f"监控目录: {SOURCE_FOLDER}")
print(f"目标目录: {DEST_FOLDER}")
print(f"文件夹前缀: '{FOLDER_PREFIX}'")
print(f"稳定性检查间隔: {STABLE_TIME}")
print(f"最大线程数: {MAX_WORKERS}\n")
# 验证目录
if not SOURCE_FOLDER.is_dir():
print(f"[错误] 源目录不存在: {SOURCE_FOLDER}")
return
try:
DEST_FOLDER.mkdir(parents=True, exist_ok=True)
except Exception as e:
print(f"[错误] 无法创建目标目录: {e}")
return
# 启动监控
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
event_handler = FolderMonitor(executor)
observer = Observer()
observer.schedule(event_handler, str(SOURCE_FOLDER), recursive=False)
observer.start()
print("[系统] 监控已启动 (按 Ctrl+C 停止)...")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n[系统] 正在停止监控...")
finally:
observer.stop()
observer.join()
print("[系统] 监控已停止")
if __name__ == "__main__":
main()

146
递归删除文件夹.py Normal file
View File

@ -0,0 +1,146 @@
import os
import shutil
import argparse
# =============== 全局路径配置 ===============
REJECT_ROOT = r"D:\videos" # 包含rejected_files.txt的根路径
DELETE_ROOT = "E:" # 要删除文件夹的根路径
# =============== 结束配置 ===============
def find_rejected_files(root_path):
"""在指定根路径下递归查找所有名为rejected_files.txt的文件"""
rejected_files = []
for root, dirs, files in os.walk(root_path):
if 'rejected_files.txt' in files:
rejected_files.append(os.path.join(root, 'rejected_files.txt'))
return rejected_files
def read_folder_names(file_paths):
"""读取多个TXT文件中的文件夹名称提取'] '后的字符(严格去空格,确保名称完全匹配)"""
folder_names = []
unique_names = set() # 用于去重
for file_path in file_paths:
if not os.path.exists(file_path):
print(f"警告:文件 {file_path} 不存在,已跳过")
continue
with open(file_path, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
raw_line = line.strip()
if not raw_line or raw_line.startswith('#'):
continue
if "] " in raw_line:
folder_name = raw_line.split("] ", 1)[1].strip()
else:
folder_name = raw_line.strip()
if folder_name and folder_name not in unique_names:
unique_names.add(folder_name)
folder_names.append(folder_name)
print(f"{os.path.basename(file_path)} 提取有效名称(第{line_num}行):'{folder_name}'")
elif not folder_name:
print(f"警告:{os.path.basename(file_path)}{line_num}行提取后为空,已忽略(原始内容:{raw_line}")
return folder_names
def find_matching_folders_recursively(root_path, target_names):
"""在指定根路径下递归查找所有名称与目标列表完全一致的文件夹"""
matching_folders = []
for root, dirs, files in os.walk(root_path):
for dir_name in dirs:
if dir_name in target_names:
folder_path = os.path.join(root, dir_name)
matching_folders.append(folder_path)
return matching_folders
def delete_matching_folders(root_path, names_to_delete):
"""递归删除指定根路径下所有名称与不通过列表完全一致的文件夹"""
if not os.path.isdir(root_path):
print(f"错误:源路径 {root_path} 不是有效目录,无法删除")
return 0
folders_to_delete = find_matching_folders_recursively(root_path, names_to_delete)
if not folders_to_delete:
print("没有找到匹配的文件夹需要删除")
return 0
print("\n找到以下匹配的文件夹:")
for i, folder in enumerate(folders_to_delete, 1):
print(f"{i}. {folder}")
# 询问用户确认删除
while True:
user_input = input("\n确定要删除以上所有文件夹吗?(y/n): ").strip().lower()
if user_input == 'y':
break
elif user_input == 'n':
print("取消删除操作")
return 0
else:
print("请输入 'y''n'")
deleted = 0
for folder_path in folders_to_delete:
try:
shutil.rmtree(folder_path)
deleted += 1
print(f"删除成功:{folder_path}")
except Exception as e:
print(f"删除失败 {folder_path}{str(e)}")
print(f"\n共成功删除 {deleted} 个文件夹(在 {root_path} 下递归查找)")
return deleted
def main():
parser = argparse.ArgumentParser(description='根据rejected_files.txt删除匹配的文件夹')
parser.add_argument('--reject_root', default=REJECT_ROOT,
help=f'包含rejected_files.txt的根路径默认{REJECT_ROOT}')
parser.add_argument('--delete_root', default=DELETE_ROOT,
help=f'要删除文件夹的根路径(默认:{DELETE_ROOT}')
args = parser.parse_args()
print("=" * 60)
print("操作说明:")
print(f"1. 将在 {args.reject_root} 下递归查找所有 rejected_files.txt 文件")
print(f"2. 收集这些文件中的所有条目作为要删除的文件夹名称")
print(f"3. 在 {args.delete_root} 下递归查找并删除匹配的文件夹")
print("=" * 60)
# 查找所有rejected_files.txt文件
rejected_files = find_rejected_files(args.reject_root)
if not rejected_files:
print(f"\n错误:在 {args.reject_root} 下未找到任何 rejected_files.txt 文件")
return
print(f"\n找到 {len(rejected_files)} 个 rejected_files.txt 文件:")
for file in rejected_files:
print(f" - {file}")
# 读取所有不通过的文件夹名称
print("\n----- 读取不通过列表 -----")
fail_folders = read_folder_names(rejected_files)
if not fail_folders:
print("\n错误未从rejected_files.txt文件中提取到任何有效文件夹名称")
return
print(f"\n共提取到 {len(fail_folders)} 个需要删除的文件夹名称")
# 在删除根路径下递归查找并删除匹配的文件夹
print(f"\n----- 在 {args.delete_root} 中执行删除操作 -----")
total_deleted = delete_matching_folders(args.delete_root, fail_folders)
print(f"\n总共成功删除 {total_deleted} 个文件夹")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,82 @@
import pyautogui
import time
import keyboard
import random
# ===========================
# 🎮 配置区域(可调参数)
# ===========================
pyautogui.FAILSAFE = True # 启用安全机制:鼠标快速移到左上角可紧急停止!
SCREEN_WIDTH, SCREEN_HEIGHT = pyautogui.size()
SCREEN_HEIGHT_MIDDLE = SCREEN_HEIGHT // 2 # 鼠标保持在屏幕竖直中央Y轴不变
MOVE_STEP = 50 * 2 # 每次移动的像素距离(越小越慢越自然,比如 1~3
MOVE_DURATION = 0.5 * 2 * 2 # 每次移动持续时间(单位:秒,越大越慢越平滑,推荐 0.2~1.0
MOVE_DELAY = 0 # 每次移动后的停顿时间(可调整,推荐 0.02~0.1
USE_RANDOM_OFFSET = False # 是否加入随机偏移(你不需要,保持 False 即可)
CONTROL_KEY = 'F1' # 控制鼠标移动的快捷键可修改为F1-F12或其他键
# ===========================
# 🧠 状态控制
# ===========================
is_moving_left = False # 控制是否正在向左移动鼠标
# ===========================
# 🖱️ 鼠标向左移动函数
# ===========================
def auto_move_left():
global is_moving_left
print(f"🖱️ 【鼠标自动左移】已启动:鼠标将缓慢向左移动...")
while is_moving_left:
current_x, current_y = pyautogui.position() # 获取当前鼠标 x, y 坐标
target_x = max(current_x - MOVE_STEP, 0) # 往左移动,但不能小于 0屏幕最左边
target_y = current_y # Y 坐标保持不变,保持在屏幕中间或当前位置
# 移动鼠标到目标位置(向左),使用平滑动画
pyautogui.moveTo(target_x, target_y, duration=MOVE_DURATION)
# 可选的停顿,模拟自然观察
time.sleep(MOVE_DELAY)
print(f"🛑 【鼠标自动左移】已停止。")
# ===========================
# ⌨️ 按键监听:启动 / 停止 左移控制
# ===========================
def toggle_left_movement():
global is_moving_left
if is_moving_left:
is_moving_left = False
print(f"⏹️ {CONTROL_KEY} 被按下:鼠标左移已停止。")
else:
is_moving_left = True
print(f"▶️ {CONTROL_KEY} 被按下:鼠标开始缓慢向左移动!")
# 在新线程中运行,避免阻塞主线程和键盘监听
import threading
threading.Thread(target=auto_move_left, daemon=True).start()
# 注册热键监听
keyboard.add_hotkey(CONTROL_KEY, toggle_left_movement)
# ===========================
# 🚀 启动脚本 & 保持运行
# ===========================
print("🎮 脚本已启动!")
print(f"🔘 按下 {CONTROL_KEY} 键:启动 / 停止 鼠标缓慢向左移动")
print("💡 鼠标会从当前位置开始,向左缓缓移动,不会重置到屏幕边缘")
print("⚠️ 安全提示你仍然可以用鼠标快速移动到屏幕左上角来紧急停止FAILSAFE 机制)")
print(" 当前设置:缓慢、平滑、自然向左移动,无随机抖动")
try:
keyboard.wait() # 保持程序运行,监听按键
except KeyboardInterrupt:
print("\n🛑 脚本已通过 Ctrl+C 停止。")
finally:
is_moving_left = False # 确保退出时停止移动