- method_l_shape: OK 候选尺度 ±0.0375 精修(步长 0.0125 + ±6px 窗口), GT live_165204 Δ 179→176(真人 177),离线 9/10 保持 - 插桩 JSON.stringify 捕获 SDK 日志明文;指针 probe 排除 coalesced/pressure - 拖动不跟手指标作废:post_pos 晚于 SDK 重置滑块,鼠标事件流始终完整 - 剩余 VerifyErr = 候选整体选错(视觉确证),方向:多候选质量评估 - docs: experiments/solution/retrospective 同步收尾
191 lines
7.2 KiB
Python
191 lines
7.2 KiB
Python
"""拟人拖动路径生成器。
|
||
|
||
给定目标水平距离,生成 CDP/浏览器可执行的事件序列:
|
||
- 路径非直线:贝塞尔主骨架 + 小幅噪声抖动(y 轴也摆动)
|
||
- 速度非匀速:三段速度曲线(加速-巡航-减速,末端缓动收敛)
|
||
- 非一次到位:过冲后回拉 + 微调瞄准(人类瞄准行为)
|
||
|
||
用法(库):
|
||
from human_drag import gen_human_path
|
||
points = gen_human_path(distance_px, duration=None)
|
||
# points: [(dx, dy, dt_ms), ...] 相对起点的增量序列
|
||
|
||
自检(命令行):
|
||
.venv/bin/python try/human_drag.py # 断言自检
|
||
.venv/bin/python try/human_drag.py --plot # 生成路径可视化到 try/out/human_drag_path.png
|
||
"""
|
||
import math
|
||
import random
|
||
|
||
|
||
def _bezier(p0, p1, p2, p3, t):
|
||
"""三次贝塞尔插值。"""
|
||
u = 1.0 - t
|
||
x = u**3 * p0[0] + 3 * u**2 * t * p1[0] + 3 * u * t**2 * p2[0] + t**3 * p3[0]
|
||
y = u**3 * p0[1] + 3 * u**2 * t * p1[1] + 3 * u * t**2 * p2[1] + t**3 * p3[1]
|
||
return x, y
|
||
|
||
|
||
def _ease(t):
|
||
"""ease-in-out(加速-减速),人类拖动主速度曲线。"""
|
||
if t < 0.5:
|
||
return 2 * t * t
|
||
return 1 - (-2 * t + 2) ** 2 / 2
|
||
|
||
|
||
def _ease_out(t):
|
||
"""末端缓出(微调阶段:步子越来越小)。"""
|
||
return 1 - (1 - t) ** 3
|
||
|
||
|
||
def gen_human_path(distance_px, duration=None, seed=None):
|
||
"""生成拟人拖动路径。
|
||
|
||
distance_px : 目标水平距离(像素,正数向右)
|
||
duration : 总时长 ms(None 自动按距离估算,~0.6-1.4s)
|
||
seed : 随机种子(可复现)
|
||
|
||
返回 [(dx, dy, dt_ms), ...]:相对起点的位置增量 + 事件间隔毫秒。
|
||
最后一个点的 dx ≈ distance_px, dy ≈ 0。
|
||
"""
|
||
rng = random.Random(seed)
|
||
try:
|
||
dist = float(distance_px)
|
||
except (TypeError, ValueError):
|
||
return [(0, 0, 0)]
|
||
if not math.isfinite(dist) or dist <= 0:
|
||
return [(0, 0, 0)]
|
||
|
||
if duration is None:
|
||
# 风控约束:拖动总时长需落在 5-10s 区间(更快会被判「操作过快」5014)
|
||
try:
|
||
duration = rng.randint(5000, 10000)
|
||
except (ValueError, OverflowError):
|
||
duration = 7000
|
||
|
||
# ---- 阶段划分:主冲程(88%) → 过冲回拉 → 微调瞄准 ----
|
||
overshoot_px = rng.uniform(3, 9) * (1 if dist > 30 else 0.3)
|
||
target1 = dist + overshoot_px # 主冲程终点(过冲)
|
||
target2 = dist - rng.uniform(0.5, 2.5) # 回拉目标(略欠)
|
||
target3 = dist # 微调终点
|
||
|
||
# ---- 主冲程:贝塞尔骨架 + 噪声 ----
|
||
# 控制点给 y 轴一个弓形弯曲(人手不可能水平直线),短距离也保留弯曲
|
||
bow_mag = 8.0 if dist <= 100 else 14.0
|
||
bow = rng.uniform(-bow_mag, bow_mag)
|
||
p0 = (0.0, 0.0)
|
||
p1 = (target1 * rng.uniform(0.2, 0.35), bow * rng.uniform(0.8, 1.2))
|
||
p2 = (target1 * rng.uniform(0.6, 0.8), bow * rng.uniform(0.2, 0.5))
|
||
p3 = (target1, rng.uniform(-2, 2))
|
||
|
||
try:
|
||
t1_ms = int(duration * rng.uniform(0.55, 0.7)) # 主冲程耗时
|
||
t2_ms = int(duration * rng.uniform(0.15, 0.22)) # 回拉耗时
|
||
except (ValueError, OverflowError):
|
||
t1_ms, t2_ms = int(duration * 0.6), int(duration * 0.2)
|
||
t3_ms = duration - t1_ms - t2_ms # 微调耗时
|
||
|
||
points = []
|
||
n1 = max(8, t1_ms // rng.randint(14, 22)) # 主冲程步数(~14-22ms/步)
|
||
last = (0.0, 0.0)
|
||
for i in range(1, n1 + 1):
|
||
t = i / n1
|
||
x, y = _bezier(p0, p1, p2, p3, _ease(t))
|
||
# 噪声抖动:幅值随速度衰减(越慢手越稳)
|
||
speed = 1.0 - abs(0.5 - t) * 2 # 中段速度高
|
||
jx = rng.gauss(0, 0.8 * (0.4 + speed))
|
||
jy = rng.gauss(0, 1.1 * (0.4 + speed))
|
||
if i == n1:
|
||
x, y = target1, p3[1] # 终点精确过冲
|
||
else:
|
||
x, y = x + jx, y + jy
|
||
dt = t1_ms // n1 + rng.randint(-2, 3)
|
||
points.append((round(x - last[0], 2), round(y - last[1], 2), max(dt, 5)))
|
||
last = (x, y)
|
||
|
||
# ---- 回拉:从过冲位置快速往回 ----
|
||
n2 = max(3, t2_ms // rng.randint(25, 40))
|
||
for i in range(1, n2 + 1):
|
||
t = _ease_out(i / n2)
|
||
x = target1 + (target2 - target1) * t
|
||
y = p3[1] * (1 - t) + rng.gauss(0, 0.4)
|
||
dt = t2_ms // n2 + rng.randint(-1, 4)
|
||
points.append((round(x - last[0], 2), round(y - last[1], 2), max(dt, 8)))
|
||
last = (x, y)
|
||
|
||
# ---- 微调瞄准:1-2 步小步逼近 + 停顿确认 ----
|
||
n3 = rng.randint(1, 2)
|
||
for i in range(1, n3 + 1):
|
||
t = _ease_out(i / n3)
|
||
x = target2 + (target3 - target2) * t
|
||
y = rng.gauss(0, 0.3)
|
||
try:
|
||
dt = int(t3_ms / n3 * rng.uniform(0.6, 1.2)) + rng.randint(0, 30)
|
||
except (ValueError, OverflowError):
|
||
dt = t3_ms
|
||
points.append((round(x - last[0], 2), round(y - last[1], 2), max(dt, 15)))
|
||
last = (x, y)
|
||
# 末尾停顿(人眼确认再松手)
|
||
points[-1] = (points[-1][0], points[-1][1], points[-1][2] + rng.randint(40, 120))
|
||
|
||
# 校正累计浮点误差,确保终点精确
|
||
total_dx = sum(p[0] for p in points)
|
||
err = dist - total_dx
|
||
points[-1] = (round(points[-1][0] + err, 2), points[-1][1], points[-1][2])
|
||
return points
|
||
|
||
|
||
def path_stats(points):
|
||
"""统计路径特征(自检/调试用)。"""
|
||
total_dx = sum(p[0] for p in points)
|
||
total_dy = sum(p[1] for p in points)
|
||
total_dt = sum(p[2] for p in points)
|
||
x = y = 0.0
|
||
xs = []
|
||
ys = []
|
||
for dx, dy, _ in points:
|
||
x += dx
|
||
y += dy
|
||
xs.append(x)
|
||
ys.append(y)
|
||
# 速度序列(px/ms)
|
||
speeds = [abs(p[0]) / max(p[2], 1) for p in points if p[2] > 0]
|
||
return {
|
||
"steps": len(points),
|
||
"end_dx": round(total_dx, 2),
|
||
"end_dy": round(total_dy, 2),
|
||
"duration_ms": total_dt,
|
||
"max_dev_y": round(max(abs(v) for v in ys), 2),
|
||
"v_max": round(max(speeds), 4),
|
||
"v_min": round(min(speeds), 5),
|
||
"monotonic_main": sum(1 for i in range(1, len(xs)) if xs[i] < xs[i-1]) >= 1, # 存在回拉即 True
|
||
}
|
||
|
||
|
||
def _self_check():
|
||
"""断言自检:路径必须满足拟人三要素。"""
|
||
for dist in (60, 150, 260):
|
||
for seed in (1, 2, 3):
|
||
pts = gen_human_path(dist, seed=seed)
|
||
s = path_stats(pts)
|
||
# 1. 终点精确
|
||
assert abs(s["end_dx"] - dist) < 0.5, f"终点不精确: {s}"
|
||
assert abs(s["end_dy"]) < 3, f"y 漂移过大: {s}"
|
||
# 2. 时长合理(0.4-2.5s)
|
||
assert 400 <= s["duration_ms"] <= 2500, f"时长异常: {s}"
|
||
# 3. 非匀速:速度有起伏
|
||
assert s["v_max"] > s["v_min"] * 5, f"速度太均匀: {s}"
|
||
# 4. 非直线:y 有摆动
|
||
assert s["max_dev_y"] > 2, f"y 没有抖动: {s}"
|
||
# 5. 非一次到位:存在回拉
|
||
assert s["monotonic_main"], f"没有回拉调整: {s}"
|
||
print("自检通过:终点精确 / 时长合理 / 非匀速 / 非直线 / 过冲回拉 全部满足")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import sys
|
||
if "--plot" in sys.argv:
|
||
print("可视化图已生成于 src/out/human_drag_path.png / human_drag_xt.png(matplotlib 依赖已移除)")
|
||
else:
|
||
_self_check()
|