mirror of
https://github.com/luoluoluo22/jianying-editor-skill.git
synced 2026-09-19 07:30:30 +08:00
feat: optimize smart zoom with clamped position, 5s hold duration and DPI awareness
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+53
-57
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
{"window_pos": "300x220+1398+660"}
|
||||
{"window_pos": "300x279+1120+1086"}
|
||||
Reference in New Issue
Block a user