feat: optimize smart zoom with clamped position, 5s hold duration and DPI awareness

This commit is contained in:
luoluoluo22
2026-01-27 20:22:40 +08:00
parent 31e539c454
commit 7dfa0f593b
8 changed files with 62 additions and 58 deletions
Binary file not shown.
Binary file not shown.
+53 -57
View File
@@ -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
+8
View File
@@ -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 文件夹,或者用户指定的目录
+1 -1
View File
@@ -1 +1 @@
{"window_pos": "300x220+1398+660"}
{"window_pos": "300x279+1120+1086"}