Files
杨豪 cae37465d2 feat: 尺度精修上线,首次机器 PASS(live_160548 s=1.0375)
- 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 同步收尾
2026-09-15 16:20:05 +08:00

191 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""拟人拖动路径生成器。
给定目标水平距离,生成 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 : 总时长 msNone 自动按距离估算,~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.pngmatplotlib 依赖已移除)")
else:
_self_check()