refactor(generator): remove deterministic speed_profile and hard log_dt clip
These post-processing hacks were added to compensate for small-data training. With Balabit pretraining they suppress the multimodal timing distribution and cause the template-y Δt curves seen in the verify UI.
This commit is contained in:
@@ -12,8 +12,8 @@ Pipeline:
|
||||
c. Enforce forward monotonicity (prevent x-axis jitter).
|
||||
6. Temporal post-processing:
|
||||
a. Clip log_dt to [0, 5] to prevent exponential explosion.
|
||||
b. Remove outliers beyond 2σ from median.
|
||||
c. Apply bell-curve speed profile (slow→fast→slow).
|
||||
(speed profile and median±1.1 hard clip removed in 2026-05 refactor —
|
||||
let the model's learned timing distribution come through naturally.)
|
||||
7. Decode to pixels via decode_trajectory.
|
||||
8. Resample to n_points if n_points != model seq_len.
|
||||
9. Convert log_dt → ms timestamps, scale to total_duration, clip [2, 150].
|
||||
@@ -257,34 +257,6 @@ def generate(
|
||||
# First point has no interval (padding from training)
|
||||
log_dt[0] = 0.0
|
||||
|
||||
# The model tends to produce exaggerated deceleration at the tail
|
||||
# (last 10 points log_dt ~3-5 vs middle ~1.5).
|
||||
# Cap the max-to-median ratio to ~3× (i.e., tail Δt ≤ 3× median Δt)
|
||||
median_ldt = float(np.median(log_dt[1:]))
|
||||
# Allow max log_dt = median + 1.1 (exp(1.1) ≈ 3× ratio)
|
||||
max_allowed = median_ldt + 1.1
|
||||
min_allowed = max(median_ldt - 1.1, 0.0)
|
||||
for i in range(1, len(log_dt)):
|
||||
if log_dt[i] > max_allowed:
|
||||
log_dt[i] = max_allowed
|
||||
elif log_dt[i] < min_allowed:
|
||||
log_dt[i] = min_allowed
|
||||
|
||||
# Apply asymmetric speed profile: start slow, fast in middle, gentle end
|
||||
# Mimics natural mouse movement (accelerate → cruise → decelerate)
|
||||
t_frac = np.linspace(0, 1, len(log_dt))
|
||||
speed_profile = np.zeros_like(log_dt, dtype=float)
|
||||
for i in range(1, len(log_dt)):
|
||||
t = t_frac[i]
|
||||
if t < 0.15:
|
||||
# Acceleration phase: start slow (+0.3 at t=0, → 0 at t=0.15)
|
||||
speed_profile[i] = 0.3 * (1.0 - t / 0.15)
|
||||
elif t > 0.85:
|
||||
# Deceleration phase: end slightly slow (+0.2 at t=1)
|
||||
speed_profile[i] = 0.2 * ((t - 0.85) / 0.15)
|
||||
# Middle: speed_profile = 0 (fastest, no penalty)
|
||||
log_dt[1:] = log_dt[1:] + speed_profile[1:]
|
||||
|
||||
# Decode spatial coordinates to pixels
|
||||
normalised = np.stack([forward, lateral], axis=1) # (seq_len, 2)
|
||||
pixels = decode_trajectory(normalised, start, end) # (seq_len, 2)
|
||||
|
||||
@@ -104,3 +104,23 @@ class TestGenerate:
|
||||
)
|
||||
# 32 move points + 2 click events = 34
|
||||
assert len(result) == 34
|
||||
|
||||
|
||||
class TestPostProcessing:
|
||||
def test_dt_diversity_preserved(self, model_dir):
|
||||
"""After removing speed_profile + median clip, multiple generations
|
||||
should differ in their Δt sequences (not all identical)."""
|
||||
results = [generate(start=(100, 200), end=(500, 400), model_dir=str(model_dir))
|
||||
for _ in range(5)]
|
||||
# Extract Δt sequences (only move events, not click events)
|
||||
dts = []
|
||||
for r in results:
|
||||
moves = r[:-2]
|
||||
dt_seq = [moves[i+1][2] - moves[i][2] for i in range(len(moves)-1)]
|
||||
dts.append(dt_seq)
|
||||
# At least 2 of the 5 sequences should differ at any given index
|
||||
for i in range(min(len(d) for d in dts)):
|
||||
values = {tuple([d[i]]) for d in dts}
|
||||
if len(values) > 1:
|
||||
return # at least one position has variation — pass
|
||||
pytest.fail("All 5 Δt sequences are identical at every position — diversity collapsed")
|
||||
|
||||
Reference in New Issue
Block a user