From 50dbf407090cda15596e80cb9a0ab2779497a27e Mon Sep 17 00:00:00 2001 From: Huang Qi Date: Sun, 10 May 2026 13:22:57 +0800 Subject: [PATCH] refactor(generator): remove deterministic speed_profile and hard log_dt clip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ai_mouse/generator.py | 32 ++------------------------------ tests/test_generator.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/ai_mouse/generator.py b/ai_mouse/generator.py index d1fd98d..56e63c1 100644 --- a/ai_mouse/generator.py +++ b/ai_mouse/generator.py @@ -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) diff --git a/tests/test_generator.py b/tests/test_generator.py index 3eb695c..4f3ce06 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -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")