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:
2026-05-10 13:22:57 +08:00
parent b01f6512c0
commit 50dbf40709
2 changed files with 22 additions and 30 deletions

View File

@@ -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)

View File

@@ -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")