import * as tf from '@tensorflow/tfjs';
import { RandomForestClassifier } from 'ml-random-forest';
import { Data } from "./data";
/**
* Represent a generic instance of a single recorded mouse action. It must contain at least
* three data : time, x and y. Other features are calculated from these three data
* in the Recorder class.
*/
export interface SingleRecord {
/**
* The time stamp when this movement was recorded, often given by `event.timeStamp`.
*/
time: number;
/**
* The pixel of the x position of the movement. If used with a normalization where the scale is the
* screen resolution, it is a number between 0 and 1, where 0 represents the left side and 1 the right side.
*/
x: number;
/**
* The pixel of the y position of the movement. If used with a normalization where the scale is the
* screen resolution, it is a number between 0 and 1, where 0 represents the left side and 1 the right side.
*/
y: number;
/**
* The type of the recorded movement, it can be "Pressed", "Released" or "Move" (ending with "Touch" if from
* a mobile device). It isn't actually used, so it can be undefined.
*/
type?: string;
/**
* The difference `dt` between two consecutive times.
*/
timeDiff?: number;
/**
* The x offset between the current point and the previous one.
*/
dx?: number;
/**
* The y offset between the current point and the previous one.
*/
dy?: number;
/**
* The speed in X axis between the current point and the previous one, calculated with `dx/dt`.
*/
speedX?: number;
/**
* The speed in Y axis between the current point and the previous one, calculated with `dy/dt`.
*/
speedY?: number;
/**
* The acceleration in X axis between the current point and the previous one, calculated with
* the difference of two consecutive speed x divided by `dt`.
*/
accelX?: number;
/**
* The acceleration in Y axis between the current point and the previous one, calculated with
* the difference of two consecutive speed y divided by `dt`.
*/
accelY?: number;
/**
* The total Euclidean distance between the current point and the previous one.
*/
distance?: number;
/**
* The total speed between the current point and the previous one, calculated with `distance/dt`.
*/
speed?: number;
/**
* The total acceleration between the current point and the previous one,
* calculated the difference of two consecutive speed divided by `dt`.
*/
accel?: number;
/**
* The difference of acceleration between the current point and the previous one divided by `dt`.
*/
jerk?: number;
/**
* The speed x divided by distance.
*/
speedXAgainstDistance?: number;
/**
* The speed y divided by distance.
*/
speedYAgainstDistance?: number;
/**
* The acceleration x divided by distance.
*/
accelXAgainstDistance?: number;
/**
* The acceleration y divided by distance.
*/
accelYAgainstDistance?: number;
/**
* The average speed from the first recorded point divided by the distance.
*/
averageSpeedAgainstDistance?: number;
/**
* The average acceleration from the first recorded point divided by the distance.
*/
averageAccelAgainstDistance?: number;
/**
* The angle between the current point and the previous one, calculated by `atan(dy/dx)`.
*/
angle?: number;
}
/**
* Represents the result of the model to decide whether the record was human or not.
* It's simply a boolean with a reason, i.e. small description of the result.
*/
export interface Result {
/**
* The simple boolean, true if the recording was considered as human. Otherwise,
* the {@link Result.reason} gives more details.
*/
result: boolean;
/**
* One of the three static field of Recorder class :
*
* - {@link Recorder.success} if the record was considered as human.
* - {@link Recorder.fail} if the record was considered as bot.
* - {@link Recorder.notEnoughProvidedData} if there were not enough provided data.
*
*/
reason: Symbol;
}
/**
* An abstract class that represents a model with its associated data used during the
* training. Concretely, you can give to the constructor a way to load a
* model with {@link getModel} and describe how to use this model to in method
* {@link predict} with parsed data from the given record and {@link data}.
*
* The model never loads if you never call {@link getModel}, so you can instantiate
* this class multiple times without troubles.
*/
export declare abstract class Model {
/**
* The model field or null if {@link getModel} has never been called.
* @protected
*/
protected model: null | G;
/**
* The way to load the model with {@link getModel}, once loaded this field
* is never used again.
* @protected
*/
protected readonly loadingPath: string;
/**
* The data object to format input.
* @protected
*/
protected readonly data: Data;
/**
* @param loadingPath The loadingPath (url, localstorage, indexeddb, ...) to load the model.
* @param data The {@link data!Data} instance related to this model.
*/
constructor(loadingPath: string, data: Data);
/**
* @Return The private field {@link data}.
*/
getData(): Data;
/**
* @Return The loaded model instance. If there was no call of {@link getModel}, throw an error instead.
*/
getLoadedModel(): G;
/**
* Loads the model (if it has never been done before) and returns it.
* @return A promise of the loaded model.
*/
abstract getModel(): Promise;
/**
* Given a record of mouse features, use both {@link data} and {@link model} fields to
* format the record and predict a list of values, each element is a probability corresponding to
* an element of the dataset to be a bot trajectory.
* @param record The record object with computed mouse features.
* @param uniqueDataset An optional boolean, if true then we reshape the dataset to have a single
* element with all our data, so the returned list should have a single element
* (a single prediction), otherwise the model predict element by element and returns
* the prediction array. All models do not support modified input shape.
*/
abstract predict(record: Recorder, uniqueDataset: boolean): Promise;
}
/**
* An implementation of {@link Model} for TensorFlow layers models.
* The loadingPath is directly sent to {@link tf.loadLayersModel} and leads to
* the json file of the model.
* @see Model
*/
export declare class TensorFlowModel extends Model {
getModel(): Promise;
predict(record: Recorder, uniqueDataset?: boolean): Promise;
}
/**
* An implementation of {@link Model} for Random Forest classifiers.
* The loadingPath is directly sent to {@link loadFile} then parsed to
* JSON format and loaded with {@link RandomForestClassifier.load}.
* @see Model
*/
export declare class RandomForestModel extends Model {
getModel(): Promise;
predict(record: Recorder, uniqueDataset: boolean): Promise;
}
/**
* Recorder class to keep track of previously recorded mouse actions, mainly for move events.
* It automatically computes all mouse features needed for {@link data!Data} format.
*
* A simple usage would be to
*
* - add an event listener for mousemove or touchmove event;
* - call {@link addRecord} with the timestamp and (x,y) coordinates every time the event fires;
* - call {@link isHuman} with a preloaded model to know if the trajectory is from human or not.
*
* Which gives something like :
* ```
* recorder = new Recorder(window.screen.width, window.screen.height);
* document.addEventListener("mousemove", event => {
* recorder.addRecord({
* time: event.timeStamp,
* x: event.clientX,
* y: event.clientY,
* type: "Move" // optional, not used in practice
* });
*
* if (recorder.getRecords().length > 100) {
* const isHuman = recorder.isHuman(delbot.Models.rnn3);
* recorder.clearRecord();
* // ...
* }
* });
* ```
* Be careful not to call `getPrediction` or `isHuman` too often, these may be heavy for smaller
* configurations.
*/
export declare class Recorder {
/**
* Static field describing a success for {@link isHuman}.
*/
static readonly success: Symbol;
/**
* Static field describing a fail for {@link isHuman}.
*/
static readonly fail: Symbol;
/**
* Static field describing an error for {@link isHuman}.
*/
static readonly notEnoughProvidedData: Symbol;
/**
* A 2-array with two numbers (a,b). When a new line with (x,y) is added
* to the current record, we compute features with (x/a, y/b).
*/
normalizer: number[];
/**
* The list of calculated mouse features from the beginning of this record.
* @private
*/
private currentRecord;
/**
* The accumulated distance from the beginning of this record, used to compute average values of mouse features.
* @private
*/
private totalDistance;
/**
* The accumulated acceleration from the beginning of this record, used to compute average values of mouse features.
* @private
*/
private totalAccel;
/**
* The accumulated speed from the beginning of this record, used to compute average values of mouse features.
* @private
*/
private totalSpeed;
/**
* The length of the trajectory, it will always be equals to `currentRecord.length`
* unless maxSize is defined and the current record is already reached.
* @private
*/
private totalLength;
/**
* The previous line used to compute the next mouse features, e.g. time diff = current time - previous time.
* @private
*/
private previousLine;
/**
* The max size of the record, default to -1 (unlimited size), to prevent high memory usage.
* If the maxSize is reached, new elements shift the entire array and are added to the end.
* @private
*/
private maxSize;
/**
* Create an empty recorder.
* @param scaleX The x resolution of the screen to keep x value between 0 and 1. If unspecified, set to 1.
* @param scaleY The y resolution of the screen to keep y value between 0 and 1. If unspecified, set to 1.
*/
constructor(scaleX?: number, scaleY?: number);
/**
* Set the max size of record. When reached, it shifts all elements.
* @param maxSize The new max size value.
*/
setMaxSize(maxSize: any): void;
/**
* Clear the current record without creating a new object.
* @return The same recorder instance for chain calls.
*/
clearRecord(): Recorder;
/**
* Get the mouse trajectory as a list of points and action
* type with their calculated features.
*/
getRecords(): SingleRecord[];
/**
* Add a line to the current record and calculate all mouse features.
* You don't have to normalize the x and y components, it is done automatically
* with the normalizer from the constructor.
* @param line The recorded action that must contain at least timestamp, x and y positions.
* @return The same recorder instance for chain calls.
*/
addRecord(line: SingleRecord): Recorder;
/**
* Load an entire trajectory as string or string array into this {@link Recorder}
* instance. If the given input is a string, we split it with the separator `\n`.
* The string array must be of the following format :
* ```
* ["resolution:1536,864",
* "9131.1,Pressed,717,361",
* "9134.8,Move,717,361",
* "9151.8,Move,717,361",
* ...
* "10402.3,Move,722,360",
* "10419.1,Move,718,358",
* "10425.8,Released,717,360]"
* ```
* As first line we have the screen resolution to normalize
* X and Y and all other lines are `timestamp,actionType,x,y`.
* We then add each line with {@link addRecord}.
*
* Notice that the instance is cleared with {@link clearRecord} when calling
* this method and the first line with resolution is optional, if absent,
* we keep the previous normalizers (1 if unspecified in constructor).
* @param recordString The string describing the trajectory.
* @param xScale If specified and > 0, the x normalization will be this value.
* @param yScale If specified and > 0, the y normalization will be this value.
* @returns The same recorder instance for chain calls.
*/
loadRecordsFromString(recordString: string | string[], xScale?: number, yScale?: number): Recorder;
/**
* This function takes a model {@link Model} containing a TensorFlow.js layer model
* and a {@link data!Data} instance and returns the list of probabilities for each batch
* element to be a bot trajectory. The batch is obtained from {@link currentRecord}.
*
* This function may be heavy for smaller configurations, be careful not to call it too often.
* @param model A {@link Model} instance, with at least `predict()`.
* @param uniqueDataset Default to false, if true then all datasets are merged into one unique,
* so there is one prediction and the average is its unique value.
* Might throw error if the classifier doesn't support it.
* @return A list of probabilities, empty list if is not enough datas.
* @see isHuman
*/
getPrediction(model: Model, uniqueDataset?: boolean): Promise;
/**
* This function takes a model {@link Model} containing a TensorFlow.js layer model
* and a {@link data!Data} instance and returns whether the trajectory stored in
* {@link currentRecord} is considered as human or bot for the model.
*
* There may be more than one input batch for the prediction, leading to more than
* one probability `p` for the sample to be a bot. It consequently takes the average
* of those probabilities and returns `true` if the average is less than a given threshold.
*
* If there is not enough input data, so we have a batch size of 0, it returns
* `false` and a reason {@link Recorder.notEnoughProvidedData}.
*
* This function may be heavy for smaller configurations, be careful not to call it too often.
* @param model A {@link Model} instance, with at least `getData()` and `getModel()`.
* @param threshold A number between 0 and 1, if the average probability to be a bot is less
* than this value, we consider the trajectory as human.
* @param uniqueDataset Default to false, if true then all datasets are merged into one unique,
* so there one prediction and the average is its unique value.
* Might throw error if the classifier doesn't support it.
* @param consoleInfo Default to false, if true then it prints the prediction to console.
* @return A promise of an instance of {@link Result} with two fields:
*
* - result, the boolean `true` iff it is a human trajectory
* - reason of the return, set to {@link Recorder.notEnoughProvidedData} if there is not enough data.
*
* @see getPrediction
*/
isHuman(model: Model, threshold?: number, uniqueDataset?: boolean, consoleInfo?: boolean): Promise;
}