diff --git a/references/pyJianYingDraft/__pycache__/keyframe.cpython-311.pyc b/references/pyJianYingDraft/__pycache__/keyframe.cpython-311.pyc index a7a98b8..eebba49 100644 Binary files a/references/pyJianYingDraft/__pycache__/keyframe.cpython-311.pyc and b/references/pyJianYingDraft/__pycache__/keyframe.cpython-311.pyc differ diff --git a/references/pyJianYingDraft/__pycache__/segment.cpython-311.pyc b/references/pyJianYingDraft/__pycache__/segment.cpython-311.pyc index 0121f74..7457b46 100644 Binary files a/references/pyJianYingDraft/__pycache__/segment.cpython-311.pyc and b/references/pyJianYingDraft/__pycache__/segment.cpython-311.pyc differ diff --git a/references/pyJianYingDraft/__pycache__/template_mode.cpython-311.pyc b/references/pyJianYingDraft/__pycache__/template_mode.cpython-311.pyc index d1596e1..cb38e9d 100644 Binary files a/references/pyJianYingDraft/__pycache__/template_mode.cpython-311.pyc and b/references/pyJianYingDraft/__pycache__/template_mode.cpython-311.pyc differ diff --git a/scripts/__pycache__/jy_wrapper.cpython-311.pyc b/scripts/__pycache__/jy_wrapper.cpython-311.pyc index 9a81def..c597d16 100644 Binary files a/scripts/__pycache__/jy_wrapper.cpython-311.pyc and b/scripts/__pycache__/jy_wrapper.cpython-311.pyc differ diff --git a/scripts/__pycache__/smart_zoomer.cpython-311.pyc b/scripts/__pycache__/smart_zoomer.cpython-311.pyc new file mode 100644 index 0000000..6302490 Binary files /dev/null and b/scripts/__pycache__/smart_zoomer.cpython-311.pyc differ diff --git a/scripts/smart_zoomer.py b/scripts/smart_zoomer.py index 83f9252..fb29f5e 100644 --- a/scripts/smart_zoomer.py +++ b/scripts/smart_zoomer.py @@ -41,7 +41,7 @@ def apply_smart_zoom(project: JyProject, video_segment, events_json_path: str, z for i in range(1, len(click_events)): prev_time = click_events[i-1]['time'] curr_time = click_events[i]['time'] - if (curr_time - prev_time) <= 3.0: + if (curr_time - prev_time) <= 5.0: current_group.append(click_events[i]) else: grouped_events.append(current_group) @@ -51,7 +51,6 @@ def apply_smart_zoom(project: JyProject, video_segment, events_json_path: str, z print(f"🔄 Grouped into {len(grouped_events)} zoom sessions.") from pyJianYingDraft.keyframe import KeyframeProperty as KP - import os # 准备红点素材路径 current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -61,13 +60,30 @@ def apply_smart_zoom(project: JyProject, video_segment, events_json_path: str, z # 缩放参数 scale_val = float(zoom_scale) / 100.0 ZOOM_IN_US = 300000 # 0.3s - HOLD_US = 3000000 # 3.0s + HOLD_US = 5000000 # 5.0s ZOOM_OUT_US = 600000 # 0.6s # 视口边界 (相对于归一化坐标中心 0.5, 0.5) - # 实际可视宽度 = 1.0 / scale - viewport_half_w = (1.0 / scale_val) / 2.0 - viewport_half_h = (1.0 / scale_val) / 2.0 + # 当缩放倍率为 S 时,屏幕可见范围在原始素材中的宽度是 1.0 / S + # 因此中心点向左向右各可见 0.5 / S + viewport_half_w = 0.5 / scale_val + viewport_half_h = 0.5 / scale_val + + def get_clamped_pos(tx, ty, scale): + """ + 计算钳制后的位置,防止出现黑边。 + tx, ty: 目标点相对于中心点的归一化偏移 (-1 to 1) + scale: 缩放倍率 (例如 1.5) + 返回: (pos_x, pos_y) 供剪映使用 + """ + px = -tx * scale + py = -ty * scale + + # 边界控制:px 必须在 [-(scale-1), (scale-1)] 之间 + limit = max(0.0, scale - 1.0) + px = max(-limit, min(px, limit)) + py = max(-limit, min(py, limit)) + return px, py for group in grouped_events: # --- 1. Start Phase (整体进场) --- @@ -79,9 +95,9 @@ def apply_smart_zoom(project: JyProject, video_segment, events_json_path: str, z video_segment.add_keyframe(KP.position_x, t_start, 0.0) video_segment.add_keyframe(KP.position_y, t_start, 0.0) - # 记录当前的摄像机中心 (归一化坐标) - current_cam_x = first_event['x'] - current_cam_y = first_event['y'] + # 记录当前的摄像机中心 (归一化坐标 0-1) + current_cam_x = 0.5 + current_cam_y = 0.5 # 遍历组内每个点击事件,以及点击之间的 Move 事件 for i, event in enumerate(group): @@ -90,61 +106,43 @@ def apply_smart_zoom(project: JyProject, video_segment, events_json_path: str, z # --- A. 添加红点标记 (Sticker) --- if os.path.exists(marker_path): try: - # 添加贴纸到新轨道,时长 0.5s - # 位置永远在屏幕中心 (0,0),因为点击时刻画面会对焦到鼠标 - # 注意:贴纸不需要缩放,保持默认大小即可 project.add_sticker_at(marker_path, t_curr_us, 500000) - except AttributeError: - # 如果 project 没实现 add_sticker_at,尝试 generic add_media - pass + except: + pass # --- B. 处理点击本身的关键帧 --- - # 目标:将本次点击位置置于中心 - target_x = (event['x'] - 0.5) * 2 - target_y = (0.5 - event['y']) * 2 - pos_x = -target_x * scale_val - pos_y = -target_y * scale_val + target_tx = (event['x'] - 0.5) * 2 + target_ty = (0.5 - event['y']) * 2 - # 更新摄像机中心 - current_cam_x = event['x'] - current_cam_y = event['y'] + pos_x, pos_y = get_clamped_pos(target_tx, target_ty, scale_val) + + # 更新摄像机中心(基于实际的平移量反推,因为可能被钳制了) + current_cam_x = -pos_x / (2 * scale_val) + 0.5 + current_cam_y = 0.5 - pos_y / (2 * scale_val) if i == 0: - # 第一帧:直接变焦 video_segment.add_keyframe(KP.uniform_scale, t_curr_us, scale_val) video_segment.add_keyframe(KP.position_x, t_curr_us, pos_x) video_segment.add_keyframe(KP.position_y, t_curr_us, pos_y) else: - # 后续帧:处理与上一次点击之间的 Move 事件 (Smart Follow) prev_event = group[i-1] t_prev_us = int(prev_event['time'] * 1000000) - - # 找出夹在 t_prev 和 t_curr 之间的 move events interval_moves = [m for m in move_events if prev_event['time'] < m['time'] < event['time']] - # 遍历中间的 move,检查是否移出视口 for m in interval_moves: t_m_us = int(m['time'] * 1000000) - - # 检查 m 点是否在当前 cam 视口内 - is_out_x = abs(m['x'] - current_cam_x) > (viewport_half_w * 0.9) # 留10%余量 - is_out_y = abs(m['y'] - current_cam_y) > (viewport_half_h * 0.9) + is_out_x = abs(m['x'] - current_cam_x) > (viewport_half_w * 0.85) + is_out_y = abs(m['y'] - current_cam_y) > (viewport_half_h * 0.85) if is_out_x or is_out_y: - # 触发跟随:将摄像机移动到该 move 点 m_tx = (m['x'] - 0.5) * 2 m_ty = (0.5 - m['y']) * 2 - m_pos_x = -m_tx * scale_val - m_pos_y = -m_ty * scale_val - - video_segment.add_keyframe(KP.position_x, t_m_us, m_pos_x) - video_segment.add_keyframe(KP.position_y, t_m_us, m_pos_y) - - # 更新当前 cam - current_cam_x = m['x'] - current_cam_y = m['y'] + m_px, m_py = get_clamped_pos(m_tx, m_ty, scale_val) + video_segment.add_keyframe(KP.position_x, t_m_us, m_px) + video_segment.add_keyframe(KP.position_y, t_m_us, m_py) + current_cam_x = -m_px / (2 * scale_val) + 0.5 + current_cam_y = 0.5 - m_py / (2 * scale_val) - # 最后确保在该 click 时刻对齐 video_segment.add_keyframe(KP.uniform_scale, t_curr_us, scale_val) video_segment.add_keyframe(KP.position_x, t_curr_us, pos_x) video_segment.add_keyframe(KP.position_y, t_curr_us, pos_y) @@ -162,7 +160,7 @@ def apply_smart_zoom(project: JyProject, video_segment, events_json_path: str, z for m in potential_moves: # 如果该移动发生在当前倒计时窗口内 (距离上一次活动 <= 3s) # 则“续费” 3s,更新最后活动时间 - if m['time'] - last_activity_time <= 3.0: + if m['time'] - last_activity_time <= 5.0: last_activity_time = m['time'] valid_post_moves.append(m) else: @@ -173,37 +171,35 @@ def apply_smart_zoom(project: JyProject, video_segment, events_json_path: str, z for m in valid_post_moves: t_m_us = int(m['time'] * 1000000) - is_out_x = abs(m['x'] - current_cam_x) > (viewport_half_w * 0.9) - is_out_y = abs(m['y'] - current_cam_y) > (viewport_half_h * 0.9) + is_out_x = abs(m['x'] - current_cam_x) > (viewport_half_w * 0.85) + is_out_y = abs(m['y'] - current_cam_y) > (viewport_half_h * 0.85) if is_out_x or is_out_y: # 触发跟随 m_tx = (m['x'] - 0.5) * 2 m_ty = (0.5 - m['y']) * 2 - m_pos_x = -m_tx * scale_val - m_pos_y = -m_ty * scale_val + m_px, m_py = get_clamped_pos(m_tx, m_ty, scale_val) - video_segment.add_keyframe(KP.position_x, t_m_us, m_pos_x) - video_segment.add_keyframe(KP.position_y, t_m_us, m_pos_y) + video_segment.add_keyframe(KP.position_x, t_m_us, m_px) + video_segment.add_keyframe(KP.position_y, t_m_us, m_py) - current_cam_x = m['x'] - current_cam_y = m['y'] + current_cam_x = -m_px / (2 * scale_val) + 0.5 + current_cam_y = 0.5 - m_py / (2 * scale_val) # 最终结束时间 = (最后一个有效活动的时刻) + 3s # 或者是: last_activity_time 已经是最后一个活动了,那么倒计时是不是指“静止 3s 后退出”? # "默认3s不缩放,期间...再次倒计时" -> 意味着 Zoom Out 发生在 last_activity_time + 3s - t_hold_end = int((last_activity_time + 3.0) * 1000000) + t_hold_end = int((last_activity_time + 5.0) * 1000000) # 获取最后时刻的各种变量用于保持状态 # 注意: 这里的 current_cam_x 已经被上面的循环更新到最新了 - final_pos_x = -((current_cam_x - 0.5) * 2) * scale_val - final_pos_y = -((current_cam_y - 0.5) * 2) * scale_val + final_px, final_py = get_clamped_pos((current_cam_x - 0.5) * 2, (0.5 - current_cam_y) * 2, scale_val) # 添加 Hold 结束帧 video_segment.add_keyframe(KP.uniform_scale, t_hold_end, scale_val) - video_segment.add_keyframe(KP.position_x, t_hold_end, final_pos_x) - video_segment.add_keyframe(KP.position_y, t_hold_end, final_pos_y) + video_segment.add_keyframe(KP.position_x, t_hold_end, final_px) + video_segment.add_keyframe(KP.position_y, t_hold_end, final_py) # 恢复全景 t_restore = t_hold_end + ZOOM_OUT_US diff --git a/tools/recording/recorder.py b/tools/recording/recorder.py index 5c9ad3a..7175bbf 100644 --- a/tools/recording/recorder.py +++ b/tools/recording/recorder.py @@ -9,6 +9,14 @@ import json import sys from pynput import mouse, keyboard +# --- Windows DPI Awareness Fix --- +if sys.platform == 'win32': + try: + import ctypes + ctypes.windll.shcore.SetProcessDpiAwareness(1) # PROCESS_SYSTEM_DPI_AWARE + except Exception: + pass + class ProGuiRecorder: def __init__(self, output_dir=None, audio_device=None): # 默认保存到当前目录下的 recordings 文件夹,或者用户指定的目录 diff --git a/tools/recording/recorder_config.json b/tools/recording/recorder_config.json index d45044b..0365d54 100644 --- a/tools/recording/recorder_config.json +++ b/tools/recording/recorder_config.json @@ -1 +1 @@ -{"window_pos": "300x220+1398+660"} \ No newline at end of file +{"window_pos": "300x279+1120+1086"} \ No newline at end of file