├── ui
├── .env.example
├── .eslintrc.json
├── app
│ ├── favicon.ico
│ ├── fonts
│ │ ├── GeistVF.woff
│ │ └── GeistMonoVF.woff
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── next.config.mjs
├── lib
│ ├── prisma.ts
│ └── utils.ts
├── postcss.config.mjs
├── actions
│ └── db.ts
├── types
│ └── path.ts
├── components.json
├── .gitignore
├── prisma
│ └── schema.prisma
├── tsconfig.json
├── tailwind.config.ts
├── components
│ └── ui
│ │ ├── toaster.tsx
│ │ ├── button.tsx
│ │ └── toast.tsx
├── package.json
├── README.md
└── hooks
│ └── use-toast.ts
├── model
├── .gitignore
├── requirements.txt
├── test.py
└── train.py
├── .DS_Store
├── puppeteer-scribe
├── .gitignore
├── server
│ ├── requirements.txt
│ └── server.py
├── package.json
├── examples
│ ├── google-search.ts
│ ├── amazon-search.ts
│ └── iframe.ts
├── index.ts
├── tsconfig.json
└── package-lock.json
├── .gitattributes
├── assets
└── banner.png
├── .vscode
└── launch.json
├── LICENSE
└── README.md
/ui/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=mongodb+srv://...
--------------------------------------------------------------------------------
/model/.gitignore:
--------------------------------------------------------------------------------
1 | .venv
2 | main.data.json
3 | models
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sameelarif/scribe/HEAD/.DS_Store
--------------------------------------------------------------------------------
/puppeteer-scribe/.gitignore:
--------------------------------------------------------------------------------
1 | model
2 | server/.venv
3 | .venv
4 | node_modules
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/assets/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sameelarif/scribe/HEAD/assets/banner.png
--------------------------------------------------------------------------------
/ui/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/ui/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sameelarif/scribe/HEAD/ui/app/favicon.ico
--------------------------------------------------------------------------------
/model/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sameelarif/scribe/HEAD/model/requirements.txt
--------------------------------------------------------------------------------
/ui/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sameelarif/scribe/HEAD/ui/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/ui/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sameelarif/scribe/HEAD/ui/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/ui/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/puppeteer-scribe/server/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sameelarif/scribe/HEAD/puppeteer-scribe/server/requirements.txt
--------------------------------------------------------------------------------
/ui/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | export default new PrismaClient({
4 | log: ["info", "error", "query", "warn"],
5 | });
6 |
--------------------------------------------------------------------------------
/ui/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/ui/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/ui/actions/db.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import prisma from "@/lib/prisma";
4 | import { PathData } from "@/types/path";
5 |
6 | export async function addPathData(pathData: PathData) {
7 | return await prisma.data.create({
8 | data: pathData,
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/ui/types/path.ts:
--------------------------------------------------------------------------------
1 | export interface Point {
2 | x: number;
3 | y: number;
4 | timestamp: number;
5 | }
6 |
7 | export interface Box {
8 | id: string;
9 | x: number;
10 | y: number;
11 | }
12 |
13 | export interface PathData {
14 | start: Point;
15 | end: Point;
16 | path: Point[];
17 | }
18 |
--------------------------------------------------------------------------------
/ui/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: Arial, Helvetica, sans-serif;
7 | }
8 |
9 | @layer utilities {
10 | .text-balance {
11 | text-wrap: balance;
12 | }
13 | }
14 |
15 | @layer base {
16 | :root {
17 | --radius: 0.5rem;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/ui/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": false,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Python Debugger: Current File",
9 | "type": "debugpy",
10 | "request": "launch",
11 | "program": "${file}",
12 | "console": "integratedTerminal",
13 | "justMyCode": false
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/ui/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "mongodb"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | type DataEnd {
11 | timestamp BigInt
12 | x Int
13 | y Int
14 | }
15 |
16 | type DataPath {
17 | timestamp BigInt
18 | x Int
19 | y Int
20 | }
21 |
22 | type DataStart {
23 | timestamp BigInt
24 | x Int
25 | y Int
26 | }
27 |
28 | model data {
29 | id String @id @default(auto()) @map("_id") @db.ObjectId
30 | end DataEnd
31 | path DataPath[]
32 | start DataStart
33 | }
34 |
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/ui/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | background: "var(--background)",
14 | foreground: "var(--foreground)",
15 | },
16 | borderRadius: {
17 | lg: "var(--radius)",
18 | md: "calc(var(--radius) - 2px)",
19 | sm: "calc(var(--radius) - 4px)",
20 | },
21 | },
22 | },
23 | plugins: [require("tailwindcss-animate")],
24 | };
25 | export default config;
26 |
--------------------------------------------------------------------------------
/puppeteer-scribe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "puppeteer-scribe",
3 | "version": "1.0.0",
4 | "main": "dist/index.js",
5 | "types": "dist/index.d.ts",
6 | "scripts": {
7 | "build": "tsc",
8 | "test": "node -e \"const args = process.argv.slice(1); require('child_process').execSync('ts-node examples/' + args.join(' '), { stdio: 'inherit' });\""
9 | },
10 | "keywords": [],
11 | "author": "sameelarif",
12 | "license": "ISC",
13 | "description": "",
14 | "dependencies": {
15 | "axios": "^1.7.7",
16 | "puppeteer": "^23.6.1"
17 | },
18 | "devDependencies": {
19 | "@types/node": "^22.8.6",
20 | "@types/puppeteer": "^5.4.7",
21 | "ts-node": "^10.9.2",
22 | "typescript": "^5.6.3"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/puppeteer-scribe/examples/google-search.ts:
--------------------------------------------------------------------------------
1 | import puppeteer from "puppeteer";
2 | import Scribe from "../index";
3 |
4 | (async () => {
5 | const browser = await puppeteer.launch({
6 | headless: false,
7 | defaultViewport: null,
8 | });
9 | const page = await browser.newPage();
10 |
11 | const scribe = new Scribe(page, {
12 | visualize: true,
13 | });
14 |
15 | // Set the viewport size to match the screen dimensions
16 | await page.setViewport({ width: 1920, height: 1080 });
17 |
18 | await page.goto("https://www.google.com");
19 |
20 | await scribe.click('textarea[title="Search"]');
21 |
22 | await scribe.type("how long are dolphins in feet");
23 |
24 | await scribe.click('input[aria-label="Google Search"]');
25 | })();
26 |
--------------------------------------------------------------------------------
/puppeteer-scribe/examples/amazon-search.ts:
--------------------------------------------------------------------------------
1 | import puppeteer from "puppeteer";
2 | import Scribe from "../index";
3 |
4 | (async () => {
5 | const browser = await puppeteer.launch({
6 | headless: false,
7 | defaultViewport: null,
8 | });
9 | const page = await browser.newPage();
10 |
11 | const scribe = new Scribe(page, {
12 | visualize: true,
13 | });
14 |
15 | await page.goto("https://www.amazon.com/");
16 |
17 | await page.waitForSelector("#twotabsearchtextbox");
18 |
19 | await scribe.click("#twotabsearchtextbox");
20 |
21 | await scribe.type("ysl myself largest most expensive bottle");
22 |
23 | await scribe.click("#nav-search-submit-button");
24 |
25 | await page.waitForNavigation();
26 |
27 | for (let i = 1; i <= 5; i++) {
28 | await scribe.click(`#a-autoid-${i}-announce`);
29 | }
30 | })();
31 |
--------------------------------------------------------------------------------
/ui/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import localFont from "next/font/local";
3 | import "./globals.css";
4 | import { Toaster } from "@/components/ui/toaster";
5 |
6 | const geistSans = localFont({
7 | src: "./fonts/GeistVF.woff",
8 | variable: "--font-geist-sans",
9 | weight: "100 900",
10 | });
11 | const geistMono = localFont({
12 | src: "./fonts/GeistMonoVF.woff",
13 | variable: "--font-geist-mono",
14 | weight: "100 900",
15 | });
16 |
17 | export const metadata: Metadata = {
18 | title: "Project Scribe",
19 | };
20 |
21 | export default function RootLayout({
22 | children,
23 | }: Readonly<{
24 | children: React.ReactNode;
25 | }>) {
26 | return (
27 |
28 |
31 | {children}
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/ui/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@prisma/client": "^5.21.1",
14 | "@radix-ui/react-icons": "^1.3.0",
15 | "@radix-ui/react-slot": "^1.1.0",
16 | "@radix-ui/react-toast": "^1.2.2",
17 | "class-variance-authority": "^0.7.0",
18 | "clsx": "^2.1.1",
19 | "lucide-react": "^0.454.0",
20 | "next": "14.2.16",
21 | "react": "^18",
22 | "react-dom": "^18",
23 | "tailwind-merge": "^2.5.4",
24 | "tailwindcss-animate": "^1.0.7"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^20",
28 | "@types/react": "^18",
29 | "@types/react-dom": "^18",
30 | "eslint": "^8",
31 | "eslint-config-next": "14.2.16",
32 | "postcss": "^8",
33 | "prisma": "^5.21.1",
34 | "tailwindcss": "^3.4.1",
35 | "typescript": "^5"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Creative Commons Attribution-NonCommercial 4.0 International License
2 |
3 | =========================================================================
4 |
5 | This license lets others remix, adapt, and build upon your work non-commercially,
6 | and although their new works must also acknowledge you and be non-commercial,
7 | they don’t have to license their derivative works on the same terms.
8 |
9 | You are free to:
10 | - Share — copy and redistribute the material in any medium or format
11 | - Adapt — remix, transform, and build upon the material
12 |
13 | Under the following terms:
14 | - Attribution — You must give appropriate credit, provide a link to the license,
15 | and indicate if changes were made. You may do so in any reasonable manner,
16 | but not in any way that suggests the licensor endorses you or your use.
17 | - NonCommercial — You may not use the material for commercial purposes.
18 |
19 | No additional restrictions — You may not apply legal terms or technological
20 | measures that legally restrict others from doing anything the license permits.
21 |
22 | To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/legalcode
23 |
--------------------------------------------------------------------------------
/puppeteer-scribe/examples/iframe.ts:
--------------------------------------------------------------------------------
1 | import puppeteer from "puppeteer";
2 | import Scribe from "../index";
3 |
4 | (async () => {
5 | const browser = await puppeteer.launch({
6 | headless: false,
7 | defaultViewport: null,
8 | });
9 | const page = await browser.newPage();
10 |
11 | const scribe = new Scribe(page, {
12 | visualize: true,
13 | });
14 |
15 | await page.setViewport({ width: 1920, height: 1080 });
16 |
17 | await page.goto(
18 | "https://hcaptcha.projecttac.com/?sitekey=27a14814-d592-444c-a711-4447baf41f48"
19 | );
20 |
21 | await new Promise((resolve) => setTimeout(resolve, 5000));
22 |
23 | await page.waitForSelector("iframe");
24 |
25 | const frames = await page.frames();
26 |
27 | let hcaptchaFrame;
28 |
29 | for (const frame of frames) {
30 | if (frame.url().includes("hcaptcha.html#frame=checkbox")) {
31 | hcaptchaFrame = frame;
32 | break;
33 | }
34 | }
35 |
36 | if (hcaptchaFrame) {
37 | await hcaptchaFrame.waitForSelector("#checkbox");
38 | await scribe.click("#checkbox", { frame: hcaptchaFrame });
39 | } else {
40 | throw new Error("hcaptchaFrame is undefined");
41 | }
42 | })();
43 |
--------------------------------------------------------------------------------
/ui/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
--------------------------------------------------------------------------------
/ui/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-neutral-300",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
14 | destructive:
15 | "bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90",
16 | outline:
17 | "border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
18 | secondary:
19 | "bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80",
20 | ghost:
21 | "hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
22 | link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2",
26 | sm: "h-8 rounded-md px-3 text-xs",
27 | lg: "h-10 rounded-md px-8",
28 | icon: "h-9 w-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | },
36 | );
37 |
38 | export interface ButtonProps
39 | extends React.ButtonHTMLAttributes,
40 | VariantProps {
41 | asChild?: boolean;
42 | }
43 |
44 | const Button = React.forwardRef(
45 | ({ className, variant, size, asChild = false, ...props }, ref) => {
46 | const Comp = asChild ? Slot : "button";
47 | return (
48 |
53 | );
54 | },
55 | );
56 | Button.displayName = "Button";
57 |
58 | export { Button, buttonVariants };
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Welcome to the **Scribe** repository! This project provides an end-to-end solution for collecting, training, and simulating human-like mouse movements in the browser. It consists of:
6 |
7 | 1. A **data collection UI** for recording user mouse movements.
8 | 2. A **machine learning model** to train and test the collected data.
9 | 3. A **Puppeteer plugin** that utilizes the trained model to produce human-like mouse movements.
10 |
11 | ## Folder Structure
12 |
13 | ### `ui/` - Data Collection Site
14 |
15 | The `ui` folder hosts the web interface for collecting mouse movement data from users.
16 |
17 | ### `model/` - Model Training and Testing Scripts
18 |
19 | The `model` folder contains the necessary scripts for training a model on the collected mouse movement data and testing the model's output.
20 |
21 | ### `puppeteer-scribe/` - Puppeteer Plugin
22 |
23 | The `puppeteer-scribe` folder contains a Puppeteer plugin that integrates a trained model to control mouse movements in automated browser tasks. This plugin sends start and end coordinates to a server (hosting the trained model), receives the generated path, and moves the cursor smoothly along the predicted trajectory.
24 |
25 | ## Installation
26 |
27 | 1. **Clone the repository:**
28 | ```bash
29 | git clone https://github.com/sameelarif/scribe.git
30 | cd scribe
31 | ```
32 | 2. **Install Dependencies**
33 | ```bash
34 | pip install -r model/requirements.txt
35 | ```
36 |
37 | ## Usage
38 |
39 | ### Data Collection
40 |
41 | The data I personally collected can be downloaded from Kaggle: https://www.kaggle.com/datasets/sameelarif/mouse-movement-between-ui-elements.
42 |
43 | If you'd like, you can run the data collection UI to collect your own data or add it to the existing dataset. To start the data collection interface:
44 |
45 | ```bash
46 | cd ui
47 | # Install dependencies
48 | npm i
49 | # Run the Next.js server
50 | npm run start
51 | ```
52 |
53 | After starting the web server, open it in your browser and use it to record your mouse movement data.
54 |
55 | ### Model Training
56 |
57 | Download the dataset to the `model` directory, and rename the `DATA_FILE` variable from `train.py` to your data file's name. To start the training:
58 |
59 | ```bash
60 | python train.py
61 | ```
62 |
63 | The model will output to `model/models`, which you can use to power `puppeteer-scribe`.
64 |
65 | ### Puppeteer Plugin Integration
66 |
67 | The `puppeteer-scribe` folder contains an example of how one would integrate the model into a browser environment. The logic can be forked and edited to support Playwright, selenium, or any other web automation library.
68 |
69 | To use the plugin, move your saved model's file into the `model` folder. You don't need to replace previous model versions as the server will automatically pick the most recent file.
70 |
71 | Assuming you already have dependencies installed, as previously shown, you can run the server:
72 |
73 | ```bash
74 | cd puppeteer-scribe
75 | # Run the server
76 | python server/server.py
77 | ```
78 |
79 | In another terminal window, you can run the test script:
80 |
81 | ```bash
82 | # Install dependencies
83 | npm i
84 | # Run the test file from `examples`
85 | npm test
86 | ```
87 |
88 | ## Contributing
89 |
90 | If you'd like to contribute, please create a pull request with a description of your changes. Your contributions will be merged as soon as they are approved.
91 |
92 | ## License
93 |
94 | This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.
95 | To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/.
96 |
97 | If you would like to use this project for commercial reasons, please contact me at `me@sameel.dev`.
98 |
--------------------------------------------------------------------------------
/ui/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/ui/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Cross2Icon } from "@radix-ui/react-icons"
5 | import * as ToastPrimitives from "@radix-ui/react-toast"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-neutral-200 p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-neutral-800",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-white text-neutral-950 dark:bg-neutral-950 dark:text-neutral-50",
33 | destructive:
34 | "destructive group border-red-500 bg-red-500 text-neutral-50 dark:border-red-900 dark:bg-red-900 dark:text-neutral-50",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/ui/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState, useEffect, useRef } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { Box, PathData, Point } from "@/types/path";
6 | import { addPathData } from "@/actions/db";
7 | import { useToast } from "@/hooks/use-toast";
8 |
9 | export default function Page() {
10 | const [boxes, setBoxes] = useState([]);
11 | const [currentBox, setCurrentBox] = useState(null);
12 | const [mousePath, setMousePath] = useState([]);
13 | const [isTracking, setIsTracking] = useState(false);
14 | const [pathData, setPathData] = useState(null);
15 | const [isMobile, setIsMobile] = useState(false);
16 | const containerRef = useRef(null);
17 | const timeoutRef = useRef(null);
18 | const { toast } = useToast();
19 |
20 | useEffect(() => {
21 | const checkIfMobile = () => {
22 | setIsMobile(
23 | window.innerWidth <= 768 ||
24 | /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
25 | );
26 | };
27 |
28 | checkIfMobile();
29 | window.addEventListener("resize", checkIfMobile);
30 | generateBoxes();
31 |
32 | return () => window.removeEventListener("resize", checkIfMobile);
33 | }, []);
34 |
35 | const generateBoxes = () => {
36 | if (containerRef.current) {
37 | const { width, height } = containerRef.current.getBoundingClientRect();
38 | const newBoxes: Box[] = [
39 | {
40 | id: "A",
41 | x: Math.random() * (width - 60),
42 | y: Math.random() * (height - 60),
43 | },
44 | {
45 | id: "B",
46 | x: Math.random() * (width - 60),
47 | y: Math.random() * (height - 60),
48 | },
49 | ];
50 | setBoxes(newBoxes);
51 | setCurrentBox("A");
52 | setMousePath([]);
53 | setIsTracking(false);
54 | setPathData(null);
55 | }
56 | };
57 |
58 | const handleBoxClick = (boxId: string) => {
59 | if (boxId === currentBox) {
60 | if (boxId === "A") {
61 | setCurrentBox("B");
62 | setIsTracking(true);
63 |
64 | timeoutRef.current = setTimeout(() => {
65 | setIsTracking(false);
66 | setCurrentBox(null);
67 |
68 | toast({
69 | title: "Timeout",
70 | description:
71 | "You must complete the task within 4 seconds. Resetting...",
72 | variant: "destructive",
73 | });
74 |
75 | setTimeout(generateBoxes, 2000);
76 | }, 4000);
77 | } else {
78 | if (timeoutRef.current) clearTimeout(timeoutRef.current);
79 |
80 | setIsTracking(false);
81 | const newPathData: PathData = {
82 | start: mousePath[0],
83 | end: mousePath[mousePath.length - 1],
84 | path: mousePath,
85 | };
86 | setPathData(newPathData);
87 | console.log("Path data:", newPathData);
88 |
89 | addPathData(newPathData);
90 |
91 | setTimeout(generateBoxes, 100); // Reset after delay
92 | }
93 | }
94 | };
95 |
96 | const handleMouseMove = (e: React.MouseEvent) => {
97 | if (isTracking && containerRef.current) {
98 | const { left, top } = containerRef.current.getBoundingClientRect();
99 | setMousePath((prev) => [
100 | ...prev,
101 | {
102 | x: e.clientX - left,
103 | y: e.clientY - top,
104 | timestamp: Date.now(),
105 | },
106 | ]);
107 | }
108 | };
109 |
110 | if (isMobile) {
111 | return (
112 |
113 |
114 | This application is only intended for desktop use with a mouse or
115 | trackpad.
116 |
117 |
118 | );
119 | }
120 |
121 | return (
122 |
123 |
Project Scribe
124 |
129 | {boxes.map((box) => (
130 |
handleBoxClick(box.id)}
143 | >
144 | {box.id}
145 |
146 | ))}
147 | {isTracking && (
148 |
149 |
153 | `${index === 0 ? "M" : "L"} ${point.x} ${point.y}`
154 | )
155 | .join(" ")}
156 | fill="none"
157 | stroke="red"
158 | strokeWidth="2"
159 | />
160 |
161 | )}
162 |
163 |
168 | Reset
169 |
170 |
175 | {currentBox === "A" && "Click point A to start tracking"}
176 | {currentBox === "B" && "Now click point B to finish"}
177 | {!currentBox && pathData && "Path recorded! Check the console for data"}
178 |
179 |
180 | );
181 | }
182 |
--------------------------------------------------------------------------------
/puppeteer-scribe/server/server.py:
--------------------------------------------------------------------------------
1 | import os
2 | import numpy as np
3 | import torch
4 | import torch.nn as nn
5 | from flask import Flask, request, jsonify
6 | import json
7 | import matplotlib
8 | matplotlib.use('Agg')
9 | import matplotlib.pyplot as plt
10 | import io
11 | import base64
12 | import glob
13 |
14 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
15 |
16 | SCREEN_WIDTH = 1920
17 | SCREEN_HEIGHT = 1080
18 |
19 | class CVAE(nn.Module):
20 | def __init__(self, input_size=2, condition_size=4, hidden_size=128, latent_size=32):
21 | super(CVAE, self).__init__()
22 | self.latent_size = latent_size
23 | self.input_size = input_size # To access input_size in methods
24 |
25 | # Encoder
26 | self.encoder_lstm = nn.LSTM(input_size + 1, hidden_size, batch_first=True) # +1 for time intervals
27 | self.fc_mu = nn.Linear(hidden_size + condition_size, latent_size)
28 | self.fc_logvar = nn.Linear(hidden_size + condition_size, latent_size)
29 |
30 | # Decoder
31 | # Input size: input_size + latent_size + condition_size + 1
32 | self.decoder_lstm = nn.LSTM(input_size + latent_size + condition_size + 1, hidden_size, batch_first=True)
33 | self.output_layer = nn.Linear(hidden_size, input_size)
34 |
35 | def decode(self, z, condition, seq_len, start_point):
36 | batch_size = z.size(0)
37 | outputs = []
38 | hidden = None
39 |
40 | # Initialize x_t with the start point
41 | x_t = start_point.to(z.device) # Shape: (batch_size, input_size)
42 |
43 | # Use average time intervals or a constant value
44 | average_delta_time = 0.05 # Adjust based on your data (in seconds)
45 | time_intervals = torch.full((batch_size, seq_len), average_delta_time).to(z.device)
46 |
47 | for t in range(seq_len):
48 | delta_t = time_intervals[:, t].unsqueeze(1) # Shape: (batch_size, 1)
49 | z_t = z # Shape: (batch_size, latent_size)
50 | cond_t = condition # Shape: (batch_size, condition_size)
51 |
52 | # Include x_t in the decoder input
53 | decoder_input = torch.cat([x_t, z_t, cond_t, delta_t], dim=-1).unsqueeze(1)
54 | # Shape: (batch_size, 1, input_size + latent_size + condition_size + 1)
55 |
56 | output, hidden = self.decoder_lstm(decoder_input, hidden)
57 | x_t = self.output_layer(output.squeeze(1)) # Shape: (batch_size, input_size)
58 | outputs.append(x_t.unsqueeze(1))
59 |
60 | # x_t is used in the next iteration
61 |
62 | outputs = torch.cat(outputs, dim=1) # Shape: (batch_size, seq_len, input_size)
63 | return outputs
64 |
65 | def generate_path(model, start_point, end_point, seq_len=50):
66 | model.eval()
67 | with torch.no_grad():
68 | condition = torch.cat([start_point, end_point], dim=-1).unsqueeze(0).to(device) # Shape: (1, 4)
69 | z = torch.randn(1, model.latent_size).to(device) # Sample from standard normal
70 |
71 | generated_seq = model.decode(z, condition, seq_len, start_point.unsqueeze(0))
72 | generated_seq = generated_seq.squeeze(0).cpu().numpy() # Shape: (seq_len, input_size)
73 | return generated_seq
74 |
75 | def transform_path_to_endpoints(path, start_point, end_point):
76 | """
77 | Transform the generated path so that it starts at start_point and ends at end_point.
78 | This is done by applying an affine transformation (rotation, scaling, and translation).
79 | """
80 | original_start = path[0]
81 | original_end = path[-1]
82 |
83 | # Vectors from start to end
84 | v_o = original_end - original_start
85 | v_d = end_point - start_point
86 |
87 | # Lengths of the vectors
88 | len_o = np.linalg.norm(v_o)
89 | len_d = np.linalg.norm(v_d)
90 |
91 | # Avoid division by zero
92 | if len_o == 0 or len_d == 0:
93 | # Return a straight line from start_point to end_point
94 | transformed_path = np.linspace(start_point, end_point, num=len(path))
95 | return transformed_path
96 |
97 | # Scale factor
98 | scale = len_d / len_o
99 |
100 | # Compute the angle between v_o and v_d
101 | # First, normalize the vectors
102 | v_o_unit = v_o / len_o
103 | v_d_unit = v_d / len_d
104 |
105 | # Compute rotation angle
106 | angle = np.arctan2(v_d_unit[1], v_d_unit[0]) - np.arctan2(v_o_unit[1], v_o_unit[0])
107 |
108 | # Build rotation matrix
109 | cos_theta = np.cos(angle)
110 | sin_theta = np.sin(angle)
111 | R = np.array([[cos_theta, -sin_theta],
112 | [sin_theta, cos_theta]])
113 |
114 | # Apply transformation to the path
115 | # Shift the path to origin based on original_start
116 | shifted_path = path - original_start
117 |
118 | # Rotate
119 | rotated_path = shifted_path @ R.T # Using matrix multiplication
120 |
121 | # Scale
122 | scaled_path = rotated_path * scale
123 |
124 | # Translate to desired start point
125 | transformed_path = scaled_path + start_point
126 |
127 | return transformed_path
128 |
129 | app = Flask(__name__)
130 |
131 | # Assuming script is being ran from parent directory
132 | script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
133 | model_dir = os.path.join(script_dir, 'model')
134 |
135 | model_files = glob.glob(os.path.join(model_dir, '*.pt'))
136 |
137 | if not model_files:
138 | raise FileNotFoundError("No model files found in the 'model' directory.")
139 |
140 | MODEL_FILE = max(model_files, key=os.path.getmtime)
141 |
142 | # Initialize the model architecture
143 | model = CVAE(input_size=2, condition_size=4, hidden_size=128, latent_size=32).to(device)
144 |
145 | # Load the saved model parameters
146 | if not os.path.exists(MODEL_FILE):
147 | raise FileNotFoundError(f"Model file {MODEL_FILE} not found. Please check the path.")
148 |
149 | model.load_state_dict(torch.load(MODEL_FILE, map_location=device))
150 | print(f"Model loaded from {MODEL_FILE}")
151 |
152 | @app.route('/generate_path', methods=['GET'])
153 | def api_generate_path():
154 | start_point = request.args.get('start_point', default=None)
155 | end_point = request.args.get('end_point', default=None)
156 | visualize = request.args.get('visualize', default='false')
157 |
158 | if not start_point or not end_point:
159 | return jsonify({'error': 'Both start and end points are required.'}), 400
160 |
161 | try:
162 | start_point = list(map(float, start_point.split(',')))
163 | end_point = list(map(float, end_point.split(',')))
164 |
165 | if not (len(start_point) == 2 and len(end_point) == 2):
166 | return jsonify({'error': 'Start and end points must have two coordinates [x, y].'}), 400
167 |
168 | # Normalize the coordinates
169 | start_point_normalized = np.array([
170 | start_point[0] / SCREEN_WIDTH,
171 | start_point[1] / SCREEN_HEIGHT
172 | ], dtype=np.float32)
173 | end_point_normalized = np.array([
174 | end_point[0] / SCREEN_WIDTH,
175 | end_point[1] / SCREEN_HEIGHT
176 | ], dtype=np.float32)
177 |
178 | start_point_tensor = torch.tensor(start_point_normalized).to(torch.float32).to(device)
179 | end_point_tensor = torch.tensor(end_point_normalized).to(torch.float32).to(device)
180 |
181 | # Generate the mouse movement path
182 | generated_path = generate_path(model, start_point_tensor, end_point_tensor, seq_len=50)
183 |
184 | # Transform the path to match the exact input start and end points
185 | transformed_path = transform_path_to_endpoints(generated_path, start_point_normalized, end_point_normalized)
186 |
187 | # Denormalize the transformed path back to screen coordinates
188 | transformed_path_denormalized = transformed_path * np.array([SCREEN_WIDTH, SCREEN_HEIGHT])
189 |
190 | # Convert the path to a list of [x, y] coordinates
191 | path_list = transformed_path_denormalized.tolist()
192 |
193 | response_data = {'path': path_list}
194 |
195 | # If visualization is requested
196 | if visualize.lower() == 'true':
197 | # Generate the plot
198 | plt.figure(figsize=(6, 6))
199 |
200 | # Plot the transformed path
201 | plt.plot(transformed_path_denormalized[:, 0], transformed_path_denormalized[:, 1],
202 | marker='o', label='Transformed Generated Path')
203 |
204 | # Plot start and end points
205 | plt.scatter(start_point[0], start_point[1],
206 | color='green', label='Start Point (Input)', s=100, zorder=5)
207 | plt.scatter(end_point[0], end_point[1],
208 | color='red', label='End Point (Input)', s=100, zorder=5)
209 |
210 | # Display coordinates
211 | plt.text(start_point[0], start_point[1] + 20,
212 | f"({start_point[0]:.1f}, {start_point[1]:.1f})",
213 | color='green')
214 | plt.text(end_point[0], end_point[1] + 20,
215 | f"({end_point[0]:.1f}, {end_point[1]:.1f})",
216 | color='red')
217 |
218 | plt.legend()
219 | plt.title('Transformed Generated Path')
220 | plt.xlabel('X Coordinate')
221 | plt.ylabel('Y Coordinate')
222 | plt.xlim(0, SCREEN_WIDTH)
223 | plt.ylim(0, SCREEN_HEIGHT)
224 | plt.gca().invert_yaxis()
225 | plt.grid(True)
226 |
227 | # Save the plot to a bytes buffer
228 | buf = io.BytesIO()
229 | plt.savefig(buf, format='png')
230 | buf.seek(0)
231 | plt.close()
232 |
233 | # Encode the image in base64
234 | img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
235 |
236 | # Add the image to the response
237 | response_data['plot'] = img_base64
238 |
239 | return jsonify(response_data), 200
240 |
241 | except Exception as e:
242 | print(f"Error: {e}")
243 | return jsonify({'error': str(e)}), 500
244 |
245 | if __name__ == '__main__':
246 | app.run(host='0.0.0.0', port=3000)
--------------------------------------------------------------------------------
/puppeteer-scribe/index.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { Frame, KeyInput, Page } from "puppeteer";
3 | import { setTimeout } from "timers/promises";
4 |
5 | interface Point {
6 | x: number;
7 | y: number;
8 | }
9 |
10 | interface MoveMouseOptions {
11 | visualize?: boolean;
12 | log?: boolean;
13 | }
14 |
15 | interface ScribeOptions {
16 | hesitationDelay: number;
17 | clickDelay: number;
18 | moveDelay: number;
19 | apiUrl: string;
20 | visualize: boolean;
21 | startPosition: Point;
22 | wpm: number;
23 | }
24 |
25 | interface TypeOptions {
26 | wpm?: number;
27 | }
28 |
29 | interface ClickOptions {
30 | frame?: Frame;
31 | }
32 |
33 | export default class Scribe {
34 | page: Page;
35 | options: ScribeOptions;
36 | mouse: Point;
37 |
38 | constructor(page: Page, options: Partial = {}) {
39 | this.page = page;
40 |
41 | const {
42 | hesitationDelay = 15,
43 | clickDelay = 50,
44 | moveDelay = 0,
45 | apiUrl = "http://localhost:3000/generate_path",
46 | visualize = false,
47 | startPosition = { x: 0, y: 0 },
48 | wpm = 80,
49 | } = options;
50 |
51 | this.options = {
52 | hesitationDelay,
53 | clickDelay,
54 | moveDelay,
55 | apiUrl,
56 | visualize,
57 | startPosition,
58 | wpm,
59 | };
60 |
61 | if (visualize) {
62 | this.installMouseHelper();
63 | }
64 |
65 | this.mouse = startPosition;
66 | this.installMouseTracker();
67 | }
68 |
69 | public async moveMouse(
70 | startPoint: Point,
71 | endPoint: Point,
72 | options: MoveMouseOptions = {}
73 | ): Promise {
74 | const { visualize = false, log = false } = options;
75 |
76 | try {
77 | const response = await axios.get(this.options.apiUrl, {
78 | params: {
79 | start_point: [startPoint.x, startPoint.y].join(","),
80 | end_point: [endPoint.x, endPoint.y].join(","),
81 | visualize: false, // We don't need the plot image
82 | },
83 | });
84 |
85 | if (response.status !== 200 || !response.data.path) {
86 | throw new Error("Failed to retrieve path from API");
87 | }
88 |
89 | const path: [number, number][] = this.interpolatePath(response.data.path);
90 |
91 | if (log) {
92 | console.log("Starting mouse movement along path");
93 | }
94 |
95 | await this.page.mouse.move(path[0][0], path[0][1]);
96 | if (log) {
97 | console.log(
98 | `Moved mouse to initial position: (${path[0][0]}, ${path[0][1]})`
99 | );
100 | }
101 |
102 | for (let i = 0; i < path.length - 1; i++) {
103 | const start = path[i];
104 | const end = path[i + 1];
105 |
106 | // Calculate distance between points
107 | const distance = Math.hypot(end[0] - start[0], end[1] - start[1]);
108 | const steps = Math.max(1, Math.floor(distance / 2));
109 |
110 | await this.page.mouse.move(end[0], end[1], { steps });
111 |
112 | if (log) {
113 | console.log(`Moved mouse to position: (${end[0]}, ${end[1]})`);
114 | }
115 |
116 | if (this.options.moveDelay > 0)
117 | await setTimeout(this.options.moveDelay);
118 | }
119 |
120 | if (log) {
121 | console.log("Completed mouse movement along path");
122 | }
123 | } catch (error) {
124 | console.error("Error in moveMouse:", error);
125 | throw error;
126 | }
127 | }
128 |
129 | public async click(
130 | selector: string,
131 | options: ClickOptions = {}
132 | ): Promise {
133 | const { frame } = options;
134 | const context = frame || this.page;
135 | const targetEl = await context.$(selector);
136 |
137 | if (!targetEl) {
138 | throw new Error(`Unable to locate element for selector: \`${selector}\``);
139 | }
140 |
141 | const targetBoundingBox = await targetEl.boundingBox();
142 |
143 | if (!targetBoundingBox) {
144 | throw new Error("No bounding box for target element");
145 | }
146 |
147 | await this.moveMouse(this.mouse, {
148 | x:
149 | targetBoundingBox.x +
150 | Math.floor(Math.random() * targetBoundingBox.width),
151 | y:
152 | targetBoundingBox.y +
153 | Math.floor(Math.random() * targetBoundingBox.height),
154 | });
155 |
156 | await this.page.mouse.down();
157 | await setTimeout(this.options.clickDelay);
158 | await this.page.mouse.up();
159 | }
160 |
161 | public async type(
162 | text: string,
163 | options: TypeOptions = { wpm: this.options.wpm }
164 | ) {
165 | const charactersPerMinute = this.options.wpm * 5;
166 | const delay = 60000 / charactersPerMinute;
167 | const dwellTime = delay / 2;
168 |
169 | const deviation = 0.2;
170 |
171 | for (let i = 0; i < text.length; i++) {
172 | const c = text.charAt(i) as KeyInput;
173 |
174 | await this.page.keyboard.down(c);
175 | await setTimeout(
176 | dwellTime + Math.random() * deviation * (Math.random() > 0.5 ? 1 : -1)
177 | );
178 | await this.page.keyboard.up(c);
179 |
180 | await setTimeout(
181 | delay -
182 | dwellTime +
183 | Math.random() * deviation * (Math.random() > 0.5 ? 1 : -1)
184 | );
185 | }
186 | }
187 |
188 | private interpolatePath(path: [number, number][]): [number, number][] {
189 | const interpolatedPath: [number, number][] = [];
190 | const totalPoints = path.length;
191 |
192 | for (let i = 0; i < totalPoints - 1; i++) {
193 | const [x1, y1] = path[i];
194 | const [x2, y2] = path[i + 1];
195 |
196 | interpolatedPath.push([x1, y1]);
197 |
198 | // Determine the number of intermediate points
199 | const distance = Math.hypot(x2 - x1, y2 - y1);
200 | const numIntermediatePoints = Math.max(1, Math.floor(distance / 10));
201 |
202 | for (let j = 1; j < numIntermediatePoints; j++) {
203 | const t = j / numIntermediatePoints;
204 | const x = x1 + (x2 - x1) * t;
205 | const y = y1 + (y2 - y1) * t;
206 | interpolatedPath.push([x, y]);
207 | }
208 | }
209 |
210 | // Add the last point
211 | interpolatedPath.push(path[totalPoints - 1]);
212 |
213 | return interpolatedPath;
214 | }
215 |
216 | private async installMouseHelper(): Promise {
217 | await this.page.evaluateOnNewDocument(() => {
218 | const attachListener = (): void => {
219 | const box = document.createElement("p-mouse-pointer");
220 | const styleElement = document.createElement("style");
221 |
222 | styleElement.innerHTML = `
223 | p-mouse-pointer {
224 | pointer-events: none;
225 | position: absolute;
226 | top: 0;
227 | z-index: 10000;
228 | left: 0;
229 | width: 20px;
230 | height: 20px;
231 | background: rgba(0,0,0,.4);
232 | border: 1px solid white;
233 | border-radius: 10px;
234 | box-sizing: border-box;
235 | margin: -10px 0 0 -10px;
236 | padding: 0;
237 | transition: background .2s, border-radius .2s, border-color .2s;
238 | }
239 | p-mouse-pointer.button-1 {
240 | transition: none;
241 | background: rgba(0,0,0,0.9);
242 | }
243 | p-mouse-pointer.button-2 {
244 | transition: none;
245 | border-color: rgba(0,0,255,0.9);
246 | }
247 | p-mouse-pointer.button-3 {
248 | transition: none;
249 | border-radius: 4px;
250 | }
251 | p-mouse-pointer.button-4 {
252 | transition: none;
253 | border-color: rgba(255,0,0,0.9);
254 | }
255 | p-mouse-pointer.button-5 {
256 | transition: none;
257 | border-color: rgba(0,255,0,0.9);
258 | }
259 | p-mouse-pointer-hide {
260 | display: none
261 | }
262 | `;
263 |
264 | document.head.appendChild(styleElement);
265 | document.body.appendChild(box);
266 |
267 | document.addEventListener(
268 | "mousemove",
269 | (event) => {
270 | console.log("event");
271 | box.style.left = String(event.pageX) + "px";
272 | box.style.top = String(event.pageY) + "px";
273 | box.classList.remove("p-mouse-pointer-hide");
274 | updateButtons(event.buttons);
275 | },
276 | true
277 | );
278 |
279 | document.addEventListener(
280 | "mousedown",
281 | (event) => {
282 | updateButtons(event.buttons);
283 | box.classList.add("button-" + String(event.which));
284 | box.classList.remove("p-mouse-pointer-hide");
285 | },
286 | true
287 | );
288 |
289 | document.addEventListener(
290 | "mouseup",
291 | (event) => {
292 | updateButtons(event.buttons);
293 | box.classList.remove("button-" + String(event.which));
294 | box.classList.remove("p-mouse-pointer-hide");
295 | },
296 | true
297 | );
298 |
299 | document.addEventListener(
300 | "mouseleave",
301 | (event) => {
302 | updateButtons(event.buttons);
303 | box.classList.add("p-mouse-pointer-hide");
304 | },
305 | true
306 | );
307 |
308 | document.addEventListener(
309 | "mouseenter",
310 | (event) => {
311 | updateButtons(event.buttons);
312 | box.classList.remove("p-mouse-pointer-hide");
313 | },
314 | true
315 | );
316 |
317 | function updateButtons(buttons: number): void {
318 | for (let i = 0; i < 5; i++) {
319 | box.classList.toggle(
320 | "button-" + String(i),
321 | Boolean(buttons & (1 << i))
322 | );
323 | }
324 | }
325 | };
326 |
327 | if (document.readyState !== "loading") {
328 | attachListener();
329 | } else {
330 | window.addEventListener("DOMContentLoaded", attachListener, false);
331 | }
332 | });
333 | }
334 |
335 | private async installMouseTracker(): Promise {
336 | await this.page.exposeFunction(
337 | "updateMousePosition",
338 | (x: number, y: number) => (this.mouse = { x, y })
339 | );
340 |
341 | await this.page.evaluateOnNewDocument(() => {
342 | window.addEventListener("mousemove", (event: MouseEvent) => {
343 | (window as any).updateMousePosition(event.pageX, event.pageY);
344 | });
345 | });
346 | }
347 | }
348 |
--------------------------------------------------------------------------------
/puppeteer-scribe/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "commonjs", /* Specify what module code is generated. */
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
42 | // "resolveJsonModule": true, /* Enable importing .json files. */
43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
45 |
46 | /* JavaScript Support */
47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
50 |
51 | /* Emit */
52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
58 | // "outDir": "./", /* Specify an output folder for all emitted files. */
59 | // "removeComments": true, /* Disable emitting comments. */
60 | // "noEmit": true, /* Disable emitting files from a compilation. */
61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
68 | // "newLine": "crlf", /* Set the newline character for emitting files. */
69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
75 |
76 | /* Interop Constraints */
77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
83 |
84 | /* Type Checking */
85 | "strict": true, /* Enable all strict type-checking options. */
86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
104 |
105 | /* Completeness */
106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/model/test.py:
--------------------------------------------------------------------------------
1 | import os
2 | import numpy as np
3 | import torch
4 | import torch.nn as nn
5 | import matplotlib.pyplot as plt
6 |
7 | # Import the spline functions for smoothing
8 | from scipy.interpolate import splprep, splev
9 |
10 | # Set device (CPU or GPU)
11 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
12 |
13 | # Define screen dimensions for normalization
14 | SCREEN_WIDTH = 1920 # Replace with your screen width if different
15 | SCREEN_HEIGHT = 1080 # Replace with your screen height if different
16 |
17 | # ====================================
18 | # 1. Model Definition
19 | # ====================================
20 |
21 | class CVAE(nn.Module):
22 | def __init__(self, input_size=2, condition_size=4, hidden_size=128, latent_size=32):
23 | super(CVAE, self).__init__()
24 | self.latent_size = latent_size
25 | self.input_size = input_size # To access input_size in methods
26 |
27 | # Encoder
28 | self.encoder_lstm = nn.LSTM(input_size + 1, hidden_size, batch_first=True) # +1 for time intervals
29 | self.fc_mu = nn.Linear(hidden_size + condition_size, latent_size)
30 | self.fc_logvar = nn.Linear(hidden_size + condition_size, latent_size)
31 |
32 | # Decoder
33 | # Input size: input_size + latent_size + condition_size + 1
34 | self.decoder_lstm = nn.LSTM(input_size + latent_size + condition_size + 1, hidden_size, batch_first=True)
35 | self.output_layer = nn.Linear(hidden_size, input_size)
36 |
37 | def encode(self, x, time_intervals, condition):
38 | batch_size = x.size(0)
39 | x = torch.cat([x, time_intervals.unsqueeze(-1)], dim=-1)
40 | packed_input = nn.utils.rnn.pack_padded_sequence(
41 | x, batch_first=True, lengths=[x.size(1)] * batch_size, enforce_sorted=False)
42 | _, (h_n, _) = self.encoder_lstm(packed_input)
43 | h_n = h_n.squeeze(0)
44 | h_n = torch.cat([h_n, condition], dim=-1)
45 | mu = self.fc_mu(h_n)
46 | logvar = self.fc_logvar(h_n)
47 | return mu, logvar
48 |
49 | def reparameterize(self, mu, logvar):
50 | std = torch.exp(0.5 * logvar)
51 | eps = torch.randn_like(std)
52 | return mu + eps * std
53 |
54 | def decode(self, z, condition, seq_len, start_point):
55 | batch_size = z.size(0)
56 | outputs = []
57 | hidden = None
58 |
59 | # Initialize x_t with the start point
60 | x_t = start_point.to(z.device) # Shape: (batch_size, input_size)
61 |
62 | # Use average time intervals or a constant value
63 | average_delta_time = 0.05 # Adjust based on your data (in seconds)
64 | time_intervals = torch.full((batch_size, seq_len), average_delta_time).to(z.device)
65 |
66 | for t in range(seq_len):
67 | delta_t = time_intervals[:, t].unsqueeze(1) # Shape: (batch_size, 1)
68 | z_t = z # Shape: (batch_size, latent_size)
69 | cond_t = condition # Shape: (batch_size, condition_size)
70 |
71 | # Include x_t in the decoder input
72 | decoder_input = torch.cat([x_t, z_t, cond_t, delta_t], dim=-1).unsqueeze(1)
73 | # Shape: (batch_size, 1, input_size + latent_size + condition_size + 1)
74 |
75 | output, hidden = self.decoder_lstm(decoder_input, hidden)
76 | x_t = self.output_layer(output.squeeze(1)) # Shape: (batch_size, input_size)
77 | outputs.append(x_t.unsqueeze(1))
78 |
79 | # x_t is used in the next iteration
80 |
81 | outputs = torch.cat(outputs, dim=1) # Shape: (batch_size, seq_len, input_size)
82 | return outputs
83 |
84 | def forward(self, x, time_intervals, condition, seq_len):
85 | # Not used during testing
86 | pass
87 |
88 | # ====================================
89 | # 2. Inference Function
90 | # ====================================
91 |
92 | def generate_path(model, start_point, end_point, seq_len=50):
93 | model.eval()
94 | with torch.no_grad():
95 | condition = torch.cat([start_point, end_point], dim=-1).unsqueeze(0).to(device) # Shape: (1, 4)
96 | z = torch.randn(1, model.latent_size).to(device) # Sample from standard normal
97 |
98 | generated_seq = model.decode(z, condition, seq_len, start_point.unsqueeze(0))
99 | generated_seq = generated_seq.squeeze(0).cpu().numpy() # Shape: (seq_len, input_size)
100 | return generated_seq
101 |
102 | # ====================================
103 | # 3. Transformation Function
104 | # ====================================
105 |
106 | def transform_path_to_endpoints(path, start_point, end_point):
107 | """
108 | Transform the generated path so that it starts at start_point and ends at end_point.
109 | This is done by applying an affine transformation (rotation, scaling, and translation).
110 | """
111 | original_start = path[0]
112 | original_end = path[-1]
113 |
114 | # Vectors from start to end
115 | v_o = original_end - original_start
116 | v_d = end_point - start_point
117 |
118 | # Lengths of the vectors
119 | len_o = np.linalg.norm(v_o)
120 | len_d = np.linalg.norm(v_d)
121 |
122 | # Avoid division by zero
123 | if len_o == 0 or len_d == 0:
124 | # Return a straight line from start_point to end_point
125 | transformed_path = np.linspace(start_point, end_point, num=len(path))
126 | return transformed_path
127 |
128 | # Scale factor
129 | scale = len_d / len_o
130 |
131 | # Compute the angle between v_o and v_d
132 | # First, normalize the vectors
133 | v_o_unit = v_o / len_o
134 | v_d_unit = v_d / len_d
135 |
136 | # Compute rotation angle
137 | angle = np.arctan2(v_d_unit[1], v_d_unit[0]) - np.arctan2(v_o_unit[1], v_o_unit[0])
138 |
139 | # Build rotation matrix
140 | cos_theta = np.cos(angle)
141 | sin_theta = np.sin(angle)
142 | R = np.array([[cos_theta, -sin_theta],
143 | [sin_theta, cos_theta]])
144 |
145 | # Apply transformation to the path
146 | # Shift the path to origin based on original_start
147 | shifted_path = path - original_start
148 |
149 | # Rotate
150 | rotated_path = shifted_path @ R.T # Using matrix multiplication
151 |
152 | # Scale
153 | scaled_path = rotated_path * scale
154 |
155 | # Translate to desired start point
156 | transformed_path = scaled_path + start_point
157 |
158 | return transformed_path
159 |
160 | # ====================================
161 | # 4. Main Testing Function
162 | # ====================================
163 |
164 | def main():
165 | # Path to the saved model file
166 | MODEL_FILE = 'models/Model Final 20241031.pt' # Replace with your model file path
167 |
168 | # Initialize the model architecture
169 | model = CVAE(input_size=2, condition_size=4, hidden_size=128, latent_size=32).to(device)
170 |
171 | # Load the saved model parameters
172 | if not os.path.exists(MODEL_FILE):
173 | print(f"Model file {MODEL_FILE} not found. Please check the path.")
174 | return
175 |
176 | model.load_state_dict(torch.load(MODEL_FILE, map_location=device))
177 | print(f"Model loaded from {MODEL_FILE}")
178 |
179 | # Input start and end points (normalized between 0 and 1)
180 | # You can modify these values as needed
181 | start_point = np.array([0.5, 0.2], dtype=np.float32) # Example start point
182 | end_point = np.array([0.5, 0.8], dtype=np.float32) # Example end point
183 |
184 | start_point_tensor = torch.tensor(start_point).to(torch.float32).to(device)
185 | end_point_tensor = torch.tensor(end_point).to(torch.float32).to(device)
186 |
187 | # Generate the mouse movement path
188 | generated_path = generate_path(model, start_point_tensor, end_point_tensor, seq_len=50)[2:]
189 |
190 | # Denormalize the coordinates for visualization or further processing
191 | generated_path_denormalized = generated_path * np.array([SCREEN_WIDTH, SCREEN_HEIGHT])
192 |
193 | # Denormalize the start and end points for printing
194 | start_point_denormalized = start_point * np.array([SCREEN_WIDTH, SCREEN_HEIGHT])
195 | end_point_denormalized = end_point * np.array([SCREEN_WIDTH, SCREEN_HEIGHT])
196 |
197 | # Transform the generated path to match the exact input start and end points
198 | transformed_path = transform_path_to_endpoints(generated_path, start_point, end_point)
199 | transformed_path_denormalized = transformed_path * np.array([SCREEN_WIDTH, SCREEN_HEIGHT])
200 |
201 | # Apply smoothing to the transformed path
202 | # Extract x and y coordinates
203 | x = transformed_path_denormalized[:, 0]
204 | y = transformed_path_denormalized[:, 1]
205 |
206 | # Smooth parameter (adjust s_value to control smoothing)
207 | s_value = 3.0 # Increase s_value for more smoothing
208 |
209 | # Generate the spline representation
210 | tck, u = splprep([x, y], s=s_value)
211 |
212 | # Evaluate the spline at a new set of points
213 | unew = np.linspace(0, 1.0, num=100)
214 | x_smooth, y_smooth = splev(unew, tck)
215 |
216 | # Combine x and y into smoothed path
217 | smoothed_path = np.vstack((x_smooth, y_smooth)).T
218 |
219 | # Print the denormalized start and end points
220 | print("Start Point (Denormalized):")
221 | print(start_point_denormalized)
222 | print("End Point (Denormalized):")
223 | print(end_point_denormalized)
224 |
225 | # Print or visualize the generated paths
226 | print("Original Generated Path (Denormalized):")
227 | print(generated_path_denormalized)
228 | print("Transformed Generated Path (Denormalized):")
229 | print(transformed_path_denormalized)
230 | print("Smoothed Transformed Path (Denormalized):")
231 | print(smoothed_path)
232 |
233 | # Visualize the paths using matplotlib
234 | plt.figure(figsize=(18, 12))
235 |
236 | # First row: plots with markers
237 | # Plot 1: Original Generated Path with markers
238 | plt.subplot(2, 3, 1)
239 | plt.plot(generated_path_denormalized[:, 0], generated_path_denormalized[:, 1],
240 | marker='o', label='Original Generated Path with Markers')
241 | # Plot start and end points after the path
242 | plt.scatter(generated_path_denormalized[0, 0], generated_path_denormalized[0, 1],
243 | color='green', label='Start Point (Generated)', s=100, zorder=5)
244 | plt.scatter(generated_path_denormalized[-1, 0], generated_path_denormalized[-1, 1],
245 | color='red', label='End Point (Generated)', s=100, zorder=5)
246 | # Display coordinates
247 | plt.text(generated_path_denormalized[0, 0], generated_path_denormalized[0, 1] + 20,
248 | f"({generated_path_denormalized[0, 0]:.1f}, {generated_path_denormalized[0, 1]:.1f})",
249 | color='green')
250 | plt.text(generated_path_denormalized[-1, 0], generated_path_denormalized[-1, 1] + 20,
251 | f"({generated_path_denormalized[-1, 0]:.1f}, {generated_path_denormalized[-1, 1]:.1f})",
252 | color='red')
253 | plt.legend()
254 | plt.title('Original Generated Path (With Markers)')
255 | plt.xlabel('X Coordinate')
256 | plt.ylabel('Y Coordinate')
257 | plt.xlim(0, SCREEN_WIDTH)
258 | plt.ylim(0, SCREEN_HEIGHT)
259 | plt.gca().invert_yaxis()
260 | plt.grid(True)
261 |
262 | # Plot 2: Transformed Generated Path with markers
263 | plt.subplot(2, 3, 2)
264 | plt.plot(transformed_path_denormalized[:, 0], transformed_path_denormalized[:, 1],
265 | marker='o', label='Transformed Generated Path with Markers')
266 | # Plot start and end points after the path
267 | plt.scatter(start_point_denormalized[0], start_point_denormalized[1],
268 | color='green', label='Start Point (Input)', s=100, zorder=5)
269 | plt.scatter(end_point_denormalized[0], end_point_denormalized[1],
270 | color='red', label='End Point (Input)', s=100, zorder=5)
271 | # Display coordinates
272 | plt.text(start_point_denormalized[0], start_point_denormalized[1] + 20,
273 | f"({start_point_denormalized[0]:.1f}, {start_point_denormalized[1]:.1f})",
274 | color='green')
275 | plt.text(end_point_denormalized[0], end_point_denormalized[1] + 20,
276 | f"({end_point_denormalized[0]:.1f}, {end_point_denormalized[1]:.1f})",
277 | color='red')
278 | plt.legend()
279 | plt.title('Transformed Generated Path (With Markers)')
280 | plt.xlabel('X Coordinate')
281 | plt.ylabel('Y Coordinate')
282 | plt.xlim(0, SCREEN_WIDTH)
283 | plt.ylim(0, SCREEN_HEIGHT)
284 | plt.gca().invert_yaxis()
285 | plt.grid(True)
286 |
287 | # Plot 3: Smoothed Transformed Path with markers
288 | plt.subplot(2, 3, 3)
289 | plt.plot(smoothed_path[:, 0], smoothed_path[:, 1],
290 | marker='o', label='Smoothed Transformed Path with Markers')
291 | # Plot start and end points after the path
292 | plt.scatter(start_point_denormalized[0], start_point_denormalized[1],
293 | color='green', label='Start Point (Input)', s=100, zorder=5)
294 | plt.scatter(end_point_denormalized[0], end_point_denormalized[1],
295 | color='red', label='End Point (Input)', s=100, zorder=5)
296 | # Display coordinates
297 | plt.text(start_point_denormalized[0], start_point_denormalized[1] + 20,
298 | f"({start_point_denormalized[0]:.1f}, {start_point_denormalized[1]:.1f})",
299 | color='green')
300 | plt.text(end_point_denormalized[0], end_point_denormalized[1] + 20,
301 | f"({end_point_denormalized[0]:.1f}, {end_point_denormalized[1]:.1f})",
302 | color='red')
303 | plt.legend()
304 | plt.title('Smoothed Transformed Path (With Markers)')
305 | plt.xlabel('X Coordinate')
306 | plt.ylabel('Y Coordinate')
307 | plt.xlim(0, SCREEN_WIDTH)
308 | plt.ylim(0, SCREEN_HEIGHT)
309 | plt.gca().invert_yaxis()
310 | plt.grid(True)
311 |
312 | # Second row: plots without markers
313 | # Plot 4: Original Generated Path without markers
314 | plt.subplot(2, 3, 4)
315 | plt.plot(generated_path_denormalized[:, 0], generated_path_denormalized[:, 1],
316 | label='Original Generated Path')
317 | # Plot start and end points after the path
318 | plt.scatter(generated_path_denormalized[0, 0], generated_path_denormalized[0, 1],
319 | color='green', label='Start Point (Generated)', s=100, zorder=5)
320 | plt.scatter(generated_path_denormalized[-1, 0], generated_path_denormalized[-1, 1],
321 | color='red', label='End Point (Generated)', s=100, zorder=5)
322 | # Display coordinates
323 | plt.text(generated_path_denormalized[0, 0], generated_path_denormalized[0, 1] + 20,
324 | f"({generated_path_denormalized[0, 0]:.1f}, {generated_path_denormalized[0, 1]:.1f})",
325 | color='green')
326 | plt.text(generated_path_denormalized[-1, 0], generated_path_denormalized[-1, 1] + 20,
327 | f"({generated_path_denormalized[-1, 0]:.1f}, {generated_path_denormalized[-1, 1]:.1f})",
328 | color='red')
329 | plt.legend()
330 | plt.title('Original Generated Path (Line Only)')
331 | plt.xlabel('X Coordinate')
332 | plt.ylabel('Y Coordinate')
333 | plt.xlim(0, SCREEN_WIDTH)
334 | plt.ylim(0, SCREEN_HEIGHT)
335 | plt.gca().invert_yaxis()
336 | plt.grid(True)
337 |
338 | # Plot 5: Transformed Generated Path without markers
339 | plt.subplot(2, 3, 5)
340 | plt.plot(transformed_path_denormalized[:, 0], transformed_path_denormalized[:, 1],
341 | label='Transformed Generated Path')
342 | # Plot start and end points after the path
343 | plt.scatter(start_point_denormalized[0], start_point_denormalized[1],
344 | color='green', label='Start Point (Input)', s=100, zorder=5)
345 | plt.scatter(end_point_denormalized[0], end_point_denormalized[1],
346 | color='red', label='End Point (Input)', s=100, zorder=5)
347 | # Display coordinates
348 | plt.text(start_point_denormalized[0], start_point_denormalized[1] + 20,
349 | f"({start_point_denormalized[0]:.1f}, {start_point_denormalized[1]:.1f})",
350 | color='green')
351 | plt.text(end_point_denormalized[0], end_point_denormalized[1] + 20,
352 | f"({end_point_denormalized[0]:.1f}, {end_point_denormalized[1]:.1f})",
353 | color='red')
354 | plt.legend()
355 | plt.title('Transformed Generated Path (Line Only)')
356 | plt.xlabel('X Coordinate')
357 | plt.ylabel('Y Coordinate')
358 | plt.xlim(0, SCREEN_WIDTH)
359 | plt.ylim(0, SCREEN_HEIGHT)
360 | plt.gca().invert_yaxis()
361 | plt.grid(True)
362 |
363 | # Plot 6: Smoothed Transformed Path without markers
364 | plt.subplot(2, 3, 6)
365 | plt.plot(smoothed_path[:, 0], smoothed_path[:, 1],
366 | label='Smoothed Transformed Path')
367 | # Plot start and end points after the path
368 | plt.scatter(start_point_denormalized[0], start_point_denormalized[1],
369 | color='green', label='Start Point (Input)', s=100, zorder=5)
370 | plt.scatter(end_point_denormalized[0], end_point_denormalized[1],
371 | color='red', label='End Point (Input)', s=100, zorder=5)
372 | # Display coordinates
373 | plt.text(start_point_denormalized[0], start_point_denormalized[1] + 20,
374 | f"({start_point_denormalized[0]:.1f}, {start_point_denormalized[1]:.1f})",
375 | color='green')
376 | plt.text(end_point_denormalized[0], end_point_denormalized[1] + 20,
377 | f"({end_point_denormalized[0]:.1f}, {end_point_denormalized[1]:.1f})",
378 | color='red')
379 | plt.legend()
380 | plt.title('Smoothed Transformed Path (Line Only)')
381 | plt.xlabel('X Coordinate')
382 | plt.ylabel('Y Coordinate')
383 | plt.xlim(0, SCREEN_WIDTH)
384 | plt.ylim(0, SCREEN_HEIGHT)
385 | plt.gca().invert_yaxis()
386 | plt.grid(True)
387 |
388 | plt.tight_layout()
389 | plt.show()
390 |
391 | if __name__ == '__main__':
392 | main()
393 |
--------------------------------------------------------------------------------
/model/train.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import numpy as np
4 | import torch
5 | import torch.nn as nn
6 | from torch.utils.data import Dataset, DataLoader
7 | from tqdm import tqdm
8 | from datetime import datetime # For timestamp
9 |
10 | # Set random seeds for reproducibility
11 | np.random.seed(42)
12 | torch.manual_seed(42)
13 |
14 | # Define screen dimensions for normalization
15 | SCREEN_WIDTH = 1920 # Replace with your screen width
16 | SCREEN_HEIGHT = 1080 # Replace with your screen height
17 |
18 | # Set device (CPU or GPU)
19 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
20 |
21 | # ====================================
22 | # 1. Data Loading and Preprocessing
23 | # ====================================
24 |
25 | class MouseMovementDataset(Dataset):
26 | def __init__(self, data_file, filter_data=False, deviation_threshold=0.05):
27 | """
28 | Args:
29 | data_file (str): Path to the data file.
30 | filter_data (bool): Whether to apply data filtering/cleaning.
31 | deviation_threshold (float): Threshold for deviation when filtering data.
32 | """
33 | self.data = []
34 | self.load_data(data_file)
35 | self.normalize_data()
36 | if filter_data:
37 | self.clean_data(deviation_threshold)
38 |
39 | def load_data(self, data_file):
40 | with open(data_file, 'r') as f:
41 | self.data = json.load(f)
42 |
43 | def normalize_data(self):
44 | for record in self.data:
45 | # Normalize start point
46 | x1 = record['start']['x']
47 | y1 = record['start']['y']
48 | x2 = record['end']['x']
49 | y2 = record['end']['y']
50 |
51 | record['start']['x'] = x1 / SCREEN_WIDTH
52 | record['start']['y'] = y1 / SCREEN_HEIGHT
53 | # Normalize end point
54 | record['end']['x'] = x2 / SCREEN_WIDTH
55 | record['end']['y'] = y2 / SCREEN_HEIGHT
56 |
57 | # Normalize path points and compute time intervals
58 | path_points = []
59 | prev_time = None
60 | for point in record['path']:
61 | x = point['x']
62 | y = point['y']
63 | t = int(point['timestamp'])
64 |
65 | x_norm = x / SCREEN_WIDTH
66 | y_norm = y / SCREEN_HEIGHT
67 |
68 | # Compute delta_time
69 | if prev_time is None:
70 | delta_time = 0.0
71 | else:
72 | delta_time = (t - prev_time) / 1000.0 # Convert ms to seconds
73 |
74 | prev_time = t
75 |
76 | # Collect the normalized point and delta_time
77 | path_points.append({'x': x_norm, 'y': y_norm, 'delta_time': delta_time})
78 |
79 | # If the first and second points are very distant, remove the first point
80 | if len(path_points) >= 2:
81 | x0, y0 = path_points[0]['x'], path_points[0]['y']
82 | x1, y1 = path_points[1]['x'], path_points[1]['y']
83 | dx = x1 - x0
84 | dy = y1 - y0
85 | distance = np.sqrt(dx**2 + dy**2)
86 | threshold = 0.05 # Threshold can be adjusted as needed
87 | if distance > threshold:
88 | # Remove the first point
89 | path_points.pop(0)
90 | # Adjust delta_time of the new first point
91 | path_points[0]['delta_time'] = 0.0
92 |
93 | # Update record['path'] with the modified path_points
94 | record['path'] = path_points
95 |
96 | def clean_data(self, deviation_threshold):
97 | """
98 | Remove samples where the path deviates significantly from a straight line.
99 | """
100 | cleaned_data = []
101 | for record in self.data:
102 | start = np.array([record['start']['x'], record['start']['y']])
103 | end = np.array([record['end']['x'], record['end']['y']])
104 | path = np.array([[p['x'], p['y']] for p in record['path']])
105 |
106 | if len(path) < 2:
107 | continue # Skip if path is too short to evaluate
108 |
109 | # Calculate deviations
110 | deviations = self.calculate_deviation(start, end, path)
111 |
112 | # Mean squared deviation
113 | msd = np.mean(deviations ** 2)
114 |
115 | if msd <= deviation_threshold:
116 | cleaned_data.append(record)
117 | else:
118 | continue # Exclude sample
119 |
120 | self.data = cleaned_data
121 |
122 | def calculate_deviation(self, start, end, path):
123 | """
124 | Calculate the perpendicular distance of each path point from the straight line between start and end.
125 | """
126 | line_vec = end - start
127 | line_len = np.linalg.norm(line_vec)
128 | if line_len == 0:
129 | return np.zeros(len(path))
130 | line_unitvec = line_vec / line_len
131 |
132 | path_vecs = path - start
133 | projections = np.dot(path_vecs, line_unitvec)
134 | projections = np.clip(projections, 0, line_len)
135 | projections_vec = np.outer(projections, line_unitvec)
136 | closest_points = start + projections_vec
137 | deviations = np.linalg.norm(path - closest_points, axis=1)
138 | return deviations
139 |
140 | def __len__(self):
141 | return len(self.data)
142 |
143 | def __getitem__(self, idx):
144 | record = self.data[idx]
145 | # Start and end points
146 | start_point = np.array([record['start']['x'], record['start']['y']], dtype=np.float32)
147 | end_point = np.array([record['end']['x'], record['end']['y']], dtype=np.float32)
148 | # Path points and time intervals
149 | path_points = []
150 | time_intervals = []
151 | for p in record['path']:
152 | path_points.append([p['x'], p['y']])
153 | time_intervals.append(p['delta_time'])
154 | path_points = np.array(path_points, dtype=np.float32) # Shape: (seq_len, 2)
155 | time_intervals = np.array(time_intervals, dtype=np.float32) # Shape: (seq_len,)
156 | return {
157 | 'start_point': start_point, # Shape: (2,)
158 | 'end_point': end_point, # Shape: (2,)
159 | 'path': path_points, # Shape: (seq_len, 2)
160 | 'time_intervals': time_intervals # Shape: (seq_len,)
161 | }
162 |
163 | # ====================================
164 | # 2. Dataset and DataLoader Preparation
165 | # ====================================
166 |
167 | def collate_fn(batch):
168 | """
169 | Custom collate function to handle variable-length sequences.
170 | """
171 | start_points = []
172 | end_points = []
173 | paths = []
174 | time_intervals = []
175 | seq_lengths = []
176 |
177 | for item in batch:
178 | start_points.append(item['start_point'])
179 | end_points.append(item['end_point'])
180 | paths.append(torch.tensor(item['path']))
181 | time_intervals.append(torch.tensor(item['time_intervals']))
182 | seq_lengths.append(len(item['path']))
183 |
184 | # Pad sequences to the maximum length in the batch
185 | max_seq_len = max(seq_lengths)
186 | padded_paths = torch.zeros(len(batch), max_seq_len, 2)
187 | padded_times = torch.zeros(len(batch), max_seq_len)
188 |
189 | for i, (path, times) in enumerate(zip(paths, time_intervals)):
190 | seq_len = seq_lengths[i]
191 | padded_paths[i, :seq_len, :] = path
192 | padded_times[i, :seq_len] = times
193 |
194 | return {
195 | 'start_points': torch.tensor(start_points), # Shape: (batch_size, 2)
196 | 'end_points': torch.tensor(end_points), # Shape: (batch_size, 2)
197 | 'paths': padded_paths, # Shape: (batch_size, max_seq_len, 2)
198 | 'time_intervals': padded_times, # Shape: (batch_size, max_seq_len)
199 | 'seq_lengths': seq_lengths
200 | }
201 |
202 | # ====================================
203 | # 3. Model Definition
204 | # ====================================
205 |
206 | class CVAE(nn.Module):
207 | def __init__(self, input_size=2, condition_size=4, hidden_size=128, latent_size=32):
208 | super(CVAE, self).__init__()
209 | self.latent_size = latent_size
210 | self.input_size = input_size # Added to access input_size in methods
211 |
212 | # Encoder
213 | self.encoder_lstm = nn.LSTM(input_size + 1, hidden_size, batch_first=True) # +1 for time intervals
214 | self.fc_mu = nn.Linear(hidden_size + condition_size, latent_size)
215 | self.fc_logvar = nn.Linear(hidden_size + condition_size, latent_size)
216 |
217 | # Decoder
218 | # Corrected input size: input_size + latent_size + condition_size + 1
219 | self.decoder_lstm = nn.LSTM(input_size + latent_size + condition_size + 1, hidden_size, batch_first=True)
220 | self.output_layer = nn.Linear(hidden_size, input_size)
221 |
222 | def encode(self, x, time_intervals, condition):
223 | batch_size = x.size(0)
224 | x = torch.cat([x, time_intervals.unsqueeze(-1)], dim=-1)
225 | packed_input = nn.utils.rnn.pack_padded_sequence(x, batch_first=True, lengths=[x.size(1)] * batch_size, enforce_sorted=False)
226 | _, (h_n, _) = self.encoder_lstm(packed_input)
227 | h_n = h_n.squeeze(0)
228 | h_n = torch.cat([h_n, condition], dim=-1)
229 | mu = self.fc_mu(h_n)
230 | logvar = self.fc_logvar(h_n)
231 | return mu, logvar
232 |
233 | def reparameterize(self, mu, logvar):
234 | std = torch.exp(0.5 * logvar)
235 | eps = torch.randn_like(std)
236 | return mu + eps * std
237 |
238 | def decode(self, z, condition, x_seq, time_intervals, seq_len):
239 | batch_size = z.size(0)
240 | outputs = []
241 | hidden = None
242 |
243 | # Initialize x_t with zeros or start tokens
244 | x_t = torch.zeros(batch_size, self.input_size).to(z.device)
245 |
246 | for t in range(seq_len):
247 | z_t = z # Shape: (batch_size, latent_size)
248 | cond_t = condition # Shape: (batch_size, condition_size)
249 | delta_t = time_intervals[:, t].unsqueeze(1) # Shape: (batch_size, 1)
250 |
251 | # Include x_t in the decoder input
252 | decoder_input = torch.cat([x_t, z_t, cond_t, delta_t], dim=-1).unsqueeze(1)
253 | # Shape: (batch_size, 1, input_size + latent_size + condition_size + 1)
254 |
255 | output, hidden = self.decoder_lstm(decoder_input, hidden)
256 | x_t = self.output_layer(output.squeeze(1)) # Shape: (batch_size, input_size)
257 | outputs.append(x_t.unsqueeze(1))
258 |
259 | # Teacher forcing: use ground truth x_t during training
260 | if self.training and x_seq is not None:
261 | x_t = x_seq[:, t, :]
262 |
263 | outputs = torch.cat(outputs, dim=1) # Shape: (batch_size, seq_len, input_size)
264 | return outputs
265 |
266 | def forward(self, x, time_intervals, condition, seq_len):
267 | mu, logvar = self.encode(x, time_intervals, condition)
268 | z = self.reparameterize(mu, logvar)
269 | recon_x = self.decode(z, condition, x_seq=x, time_intervals=time_intervals, seq_len=seq_len)
270 | return recon_x, mu, logvar
271 |
272 | # ====================================
273 | # 4. Training Loop
274 | # ====================================
275 |
276 | def loss_function(recon_x, x, mu, logvar, seq_lengths):
277 | MSE = 0
278 | total_length = sum(seq_lengths)
279 | for i in range(len(seq_lengths)):
280 | seq_len = seq_lengths[i]
281 | MSE += nn.functional.mse_loss(recon_x[i, :seq_len, :], x[i, :seq_len, :], reduction='sum')
282 | MSE /= total_length
283 | # KL Divergence
284 | KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
285 | KLD /= len(seq_lengths)
286 | return MSE + KLD
287 |
288 | def train_model(model, dataloader, num_epochs=20, learning_rate=0.001, model_save_path='models'):
289 | optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
290 | model.train()
291 |
292 | # Ensure the models directory exists
293 | if not os.path.exists(model_save_path):
294 | os.makedirs(model_save_path)
295 |
296 | for epoch in range(num_epochs):
297 | epoch_loss = 0
298 | for batch in tqdm(dataloader, desc=f'Epoch {epoch+1}/{num_epochs}'):
299 | start_points = batch['start_points'].to(device)
300 | end_points = batch['end_points'].to(device)
301 | paths = batch['paths'].to(device)
302 | time_intervals = batch['time_intervals'].to(device)
303 | seq_lengths = batch['seq_lengths']
304 | batch_size = paths.size(0)
305 | max_seq_len = paths.size(1)
306 |
307 | condition = torch.cat([start_points, end_points], dim=-1) # Shape: (batch_size, 4)
308 | optimizer.zero_grad()
309 | recon_paths, mu, logvar = model(paths, time_intervals, condition, max_seq_len)
310 | loss = loss_function(recon_paths, paths, mu, logvar, seq_lengths)
311 | loss.backward()
312 | optimizer.step()
313 | epoch_loss += loss.item()
314 |
315 | avg_loss = epoch_loss / len(dataloader)
316 | print(f'Epoch {epoch+1}/{num_epochs}, Loss: {avg_loss:.4f}')
317 |
318 | # Save the finalized model after training
319 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
320 | model_filename = f'model_final_{timestamp}.pt'
321 | model_path = os.path.join(model_save_path, model_filename)
322 | torch.save(model.state_dict(), model_path)
323 | print(f'Final model saved to {model_path}')
324 |
325 | # ====================================
326 | # 5. Inference Function
327 | # ====================================
328 |
329 | def generate_path(model, start_point, end_point, seq_len=50):
330 | model.eval()
331 | with torch.no_grad():
332 | condition = torch.cat([start_point, end_point], dim=-1).unsqueeze(0).to(device) # Shape: (1, 4)
333 | z = torch.randn(1, model.latent_size).to(device) # Sample from standard normal
334 |
335 | # Use average time intervals or a constant value
336 | average_delta_time = 0.05 # Adjust based on your data (in seconds)
337 | time_intervals = torch.full((1, seq_len), average_delta_time).to(device)
338 |
339 | outputs = []
340 | hidden = None
341 |
342 | # Initialize x_t with the start point
343 | x_t = start_point.unsqueeze(0).to(device) # Shape: (1, 2)
344 |
345 | for t in range(seq_len):
346 | delta_t = time_intervals[:, t].unsqueeze(1) # Shape: (1, 1)
347 |
348 | # Include x_t in the decoder input
349 | decoder_input = torch.cat([x_t, z, condition, delta_t], dim=-1).unsqueeze(1)
350 | # Shape: (1, 1, input_size + latent_size + condition_size + 1)
351 |
352 | output, hidden = model.decoder_lstm(decoder_input, hidden)
353 | x_t = model.output_layer(output.squeeze(1)) # Shape: (1, input_size)
354 | outputs.append(x_t.squeeze(0).cpu().numpy())
355 |
356 | # x_t is used in the next iteration
357 |
358 | generated_path = np.array(outputs)
359 | return generated_path
360 |
361 | # ====================================
362 | # 6. Main Function
363 | # ====================================
364 |
365 | def main():
366 | # Path to your data file
367 | DATA_FILE = 'main.data.json' # Replace with your data file path
368 |
369 | # Create dataset and dataloader
370 | dataset = MouseMovementDataset(DATA_FILE, filter_data=True, deviation_threshold=0.05)
371 | if len(dataset) == 0:
372 | print("No data available after filtering. Adjust your deviation_threshold or check your data.")
373 | return
374 |
375 | print("Remaining data length after filtering:", len(dataset))
376 |
377 | dataloader = DataLoader(dataset, batch_size=32, shuffle=True, collate_fn=collate_fn)
378 |
379 | # Initialize the model
380 | model = CVAE(input_size=2, condition_size=4, hidden_size=128, latent_size=32).to(device)
381 |
382 | # Train the model
383 | train_model(model, dataloader, num_epochs=20, learning_rate=0.001, model_save_path='models')
384 |
385 | # Example inference
386 | # Use normalized coordinates for start and end points
387 | start_point = np.array([0.4, 0.4], dtype=np.float32)
388 | end_point = np.array([0.4, 0.6], dtype=np.float32)
389 |
390 | start_point_tensor = torch.tensor(start_point).to(torch.float32).to(device)
391 | end_point_tensor = torch.tensor(end_point).to(torch.float32).to(device)
392 |
393 | generated_path = generate_path(model, start_point_tensor, end_point_tensor, seq_len=50)
394 |
395 | # Denormalize the coordinates for visualization or further processing
396 | generated_path_denormalized = generated_path * np.array([SCREEN_WIDTH, SCREEN_HEIGHT])
397 |
398 | # Print or visualize the generated path
399 | print("Generated Path:")
400 | print(generated_path_denormalized)
401 |
402 | # Denormalize the start and end points for printing
403 | start_point_denormalized = start_point * np.array([SCREEN_WIDTH, SCREEN_HEIGHT])
404 | end_point_denormalized = end_point * np.array([SCREEN_WIDTH, SCREEN_HEIGHT])
405 |
406 | # Print the denormalized start and end points
407 | print("Start Point (Denormalized):")
408 | print(start_point_denormalized)
409 | print("End Point (Denormalized):")
410 | print(end_point_denormalized)
411 |
412 | # Optionally, visualize the path using matplotlib
413 | try:
414 | import matplotlib.pyplot as plt
415 | plt.figure(figsize=(8, 6))
416 | plt.plot(generated_path_denormalized[:, 0], generated_path_denormalized[:, 1], marker='o', label='Generated Path')
417 | plt.scatter(start_point_denormalized[0], start_point_denormalized[1], color='green', label='Start Point')
418 | plt.scatter(end_point_denormalized[0], end_point_denormalized[1], color='red', label='End Point')
419 | plt.legend()
420 | plt.title('Generated Mouse Movement Path')
421 | plt.xlabel('X Coordinate')
422 | plt.ylabel('Y Coordinate')
423 | plt.gca().invert_yaxis()
424 | plt.grid(True)
425 | plt.show()
426 | except ImportError:
427 | print("Matplotlib is not installed. Install it to visualize the path.")
428 |
429 | if __name__ == '__main__':
430 | main()
431 |
--------------------------------------------------------------------------------
/puppeteer-scribe/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "puppeteer-scribe",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "puppeteer-scribe",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "axios": "^1.7.7",
13 | "puppeteer": "^23.6.1"
14 | },
15 | "devDependencies": {
16 | "@types/node": "^22.8.6",
17 | "@types/puppeteer": "^5.4.7",
18 | "ts-node": "^10.9.2",
19 | "typescript": "^5.6.3"
20 | }
21 | },
22 | "node_modules/@babel/code-frame": {
23 | "version": "7.26.2",
24 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
25 | "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
26 | "license": "MIT",
27 | "dependencies": {
28 | "@babel/helper-validator-identifier": "^7.25.9",
29 | "js-tokens": "^4.0.0",
30 | "picocolors": "^1.0.0"
31 | },
32 | "engines": {
33 | "node": ">=6.9.0"
34 | }
35 | },
36 | "node_modules/@babel/helper-validator-identifier": {
37 | "version": "7.25.9",
38 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
39 | "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
40 | "license": "MIT",
41 | "engines": {
42 | "node": ">=6.9.0"
43 | }
44 | },
45 | "node_modules/@cspotcode/source-map-support": {
46 | "version": "0.8.1",
47 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
48 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
49 | "dev": true,
50 | "license": "MIT",
51 | "dependencies": {
52 | "@jridgewell/trace-mapping": "0.3.9"
53 | },
54 | "engines": {
55 | "node": ">=12"
56 | }
57 | },
58 | "node_modules/@jridgewell/resolve-uri": {
59 | "version": "3.1.2",
60 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
61 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
62 | "dev": true,
63 | "license": "MIT",
64 | "engines": {
65 | "node": ">=6.0.0"
66 | }
67 | },
68 | "node_modules/@jridgewell/sourcemap-codec": {
69 | "version": "1.5.0",
70 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
71 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
72 | "dev": true,
73 | "license": "MIT"
74 | },
75 | "node_modules/@jridgewell/trace-mapping": {
76 | "version": "0.3.9",
77 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
78 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
79 | "dev": true,
80 | "license": "MIT",
81 | "dependencies": {
82 | "@jridgewell/resolve-uri": "^3.0.3",
83 | "@jridgewell/sourcemap-codec": "^1.4.10"
84 | }
85 | },
86 | "node_modules/@puppeteer/browsers": {
87 | "version": "2.4.0",
88 | "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz",
89 | "integrity": "sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==",
90 | "license": "Apache-2.0",
91 | "dependencies": {
92 | "debug": "^4.3.6",
93 | "extract-zip": "^2.0.1",
94 | "progress": "^2.0.3",
95 | "proxy-agent": "^6.4.0",
96 | "semver": "^7.6.3",
97 | "tar-fs": "^3.0.6",
98 | "unbzip2-stream": "^1.4.3",
99 | "yargs": "^17.7.2"
100 | },
101 | "bin": {
102 | "browsers": "lib/cjs/main-cli.js"
103 | },
104 | "engines": {
105 | "node": ">=18"
106 | }
107 | },
108 | "node_modules/@tootallnate/quickjs-emscripten": {
109 | "version": "0.23.0",
110 | "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
111 | "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
112 | "license": "MIT"
113 | },
114 | "node_modules/@tsconfig/node10": {
115 | "version": "1.0.11",
116 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
117 | "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
118 | "dev": true,
119 | "license": "MIT"
120 | },
121 | "node_modules/@tsconfig/node12": {
122 | "version": "1.0.11",
123 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
124 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
125 | "dev": true,
126 | "license": "MIT"
127 | },
128 | "node_modules/@tsconfig/node14": {
129 | "version": "1.0.3",
130 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
131 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
132 | "dev": true,
133 | "license": "MIT"
134 | },
135 | "node_modules/@tsconfig/node16": {
136 | "version": "1.0.4",
137 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
138 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
139 | "dev": true,
140 | "license": "MIT"
141 | },
142 | "node_modules/@types/node": {
143 | "version": "22.8.6",
144 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz",
145 | "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==",
146 | "devOptional": true,
147 | "license": "MIT",
148 | "dependencies": {
149 | "undici-types": "~6.19.8"
150 | }
151 | },
152 | "node_modules/@types/puppeteer": {
153 | "version": "5.4.7",
154 | "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.7.tgz",
155 | "integrity": "sha512-JdGWZZYL0vKapXF4oQTC5hLVNfOgdPrqeZ1BiQnGk5cB7HeE91EWUiTdVSdQPobRN8rIcdffjiOgCYJ/S8QrnQ==",
156 | "dev": true,
157 | "license": "MIT",
158 | "dependencies": {
159 | "@types/node": "*"
160 | }
161 | },
162 | "node_modules/@types/yauzl": {
163 | "version": "2.10.3",
164 | "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
165 | "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
166 | "license": "MIT",
167 | "optional": true,
168 | "dependencies": {
169 | "@types/node": "*"
170 | }
171 | },
172 | "node_modules/acorn": {
173 | "version": "8.14.0",
174 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
175 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
176 | "dev": true,
177 | "license": "MIT",
178 | "bin": {
179 | "acorn": "bin/acorn"
180 | },
181 | "engines": {
182 | "node": ">=0.4.0"
183 | }
184 | },
185 | "node_modules/acorn-walk": {
186 | "version": "8.3.4",
187 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
188 | "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
189 | "dev": true,
190 | "license": "MIT",
191 | "dependencies": {
192 | "acorn": "^8.11.0"
193 | },
194 | "engines": {
195 | "node": ">=0.4.0"
196 | }
197 | },
198 | "node_modules/agent-base": {
199 | "version": "7.1.1",
200 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
201 | "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
202 | "license": "MIT",
203 | "dependencies": {
204 | "debug": "^4.3.4"
205 | },
206 | "engines": {
207 | "node": ">= 14"
208 | }
209 | },
210 | "node_modules/ansi-regex": {
211 | "version": "5.0.1",
212 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
213 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
214 | "license": "MIT",
215 | "engines": {
216 | "node": ">=8"
217 | }
218 | },
219 | "node_modules/ansi-styles": {
220 | "version": "4.3.0",
221 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
222 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
223 | "license": "MIT",
224 | "dependencies": {
225 | "color-convert": "^2.0.1"
226 | },
227 | "engines": {
228 | "node": ">=8"
229 | },
230 | "funding": {
231 | "url": "https://github.com/chalk/ansi-styles?sponsor=1"
232 | }
233 | },
234 | "node_modules/arg": {
235 | "version": "4.1.3",
236 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
237 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
238 | "dev": true,
239 | "license": "MIT"
240 | },
241 | "node_modules/argparse": {
242 | "version": "2.0.1",
243 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
244 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
245 | "license": "Python-2.0"
246 | },
247 | "node_modules/ast-types": {
248 | "version": "0.13.4",
249 | "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
250 | "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
251 | "license": "MIT",
252 | "dependencies": {
253 | "tslib": "^2.0.1"
254 | },
255 | "engines": {
256 | "node": ">=4"
257 | }
258 | },
259 | "node_modules/asynckit": {
260 | "version": "0.4.0",
261 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
262 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
263 | "license": "MIT"
264 | },
265 | "node_modules/axios": {
266 | "version": "1.7.7",
267 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
268 | "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
269 | "license": "MIT",
270 | "dependencies": {
271 | "follow-redirects": "^1.15.6",
272 | "form-data": "^4.0.0",
273 | "proxy-from-env": "^1.1.0"
274 | }
275 | },
276 | "node_modules/b4a": {
277 | "version": "1.6.7",
278 | "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
279 | "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
280 | "license": "Apache-2.0"
281 | },
282 | "node_modules/bare-events": {
283 | "version": "2.5.0",
284 | "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz",
285 | "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==",
286 | "license": "Apache-2.0",
287 | "optional": true
288 | },
289 | "node_modules/bare-fs": {
290 | "version": "2.3.5",
291 | "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
292 | "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
293 | "license": "Apache-2.0",
294 | "optional": true,
295 | "dependencies": {
296 | "bare-events": "^2.0.0",
297 | "bare-path": "^2.0.0",
298 | "bare-stream": "^2.0.0"
299 | }
300 | },
301 | "node_modules/bare-os": {
302 | "version": "2.4.4",
303 | "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
304 | "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
305 | "license": "Apache-2.0",
306 | "optional": true
307 | },
308 | "node_modules/bare-path": {
309 | "version": "2.1.3",
310 | "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
311 | "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
312 | "license": "Apache-2.0",
313 | "optional": true,
314 | "dependencies": {
315 | "bare-os": "^2.1.0"
316 | }
317 | },
318 | "node_modules/bare-stream": {
319 | "version": "2.3.2",
320 | "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.2.tgz",
321 | "integrity": "sha512-EFZHSIBkDgSHIwj2l2QZfP4U5OcD4xFAOwhSb/vlr9PIqyGJGvB/nfClJbcnh3EY4jtPE4zsb5ztae96bVF79A==",
322 | "license": "Apache-2.0",
323 | "optional": true,
324 | "dependencies": {
325 | "streamx": "^2.20.0"
326 | }
327 | },
328 | "node_modules/base64-js": {
329 | "version": "1.5.1",
330 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
331 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
332 | "funding": [
333 | {
334 | "type": "github",
335 | "url": "https://github.com/sponsors/feross"
336 | },
337 | {
338 | "type": "patreon",
339 | "url": "https://www.patreon.com/feross"
340 | },
341 | {
342 | "type": "consulting",
343 | "url": "https://feross.org/support"
344 | }
345 | ],
346 | "license": "MIT"
347 | },
348 | "node_modules/basic-ftp": {
349 | "version": "5.0.5",
350 | "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
351 | "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
352 | "license": "MIT",
353 | "engines": {
354 | "node": ">=10.0.0"
355 | }
356 | },
357 | "node_modules/buffer": {
358 | "version": "5.7.1",
359 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
360 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
361 | "funding": [
362 | {
363 | "type": "github",
364 | "url": "https://github.com/sponsors/feross"
365 | },
366 | {
367 | "type": "patreon",
368 | "url": "https://www.patreon.com/feross"
369 | },
370 | {
371 | "type": "consulting",
372 | "url": "https://feross.org/support"
373 | }
374 | ],
375 | "license": "MIT",
376 | "dependencies": {
377 | "base64-js": "^1.3.1",
378 | "ieee754": "^1.1.13"
379 | }
380 | },
381 | "node_modules/buffer-crc32": {
382 | "version": "0.2.13",
383 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
384 | "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
385 | "license": "MIT",
386 | "engines": {
387 | "node": "*"
388 | }
389 | },
390 | "node_modules/callsites": {
391 | "version": "3.1.0",
392 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
393 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
394 | "license": "MIT",
395 | "engines": {
396 | "node": ">=6"
397 | }
398 | },
399 | "node_modules/chromium-bidi": {
400 | "version": "0.8.0",
401 | "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.8.0.tgz",
402 | "integrity": "sha512-uJydbGdTw0DEUjhoogGveneJVWX/9YuqkWePzMmkBYwtdAqo5d3J/ovNKFr+/2hWXYmYCr6it8mSSTIj6SS6Ug==",
403 | "license": "Apache-2.0",
404 | "dependencies": {
405 | "mitt": "3.0.1",
406 | "urlpattern-polyfill": "10.0.0",
407 | "zod": "3.23.8"
408 | },
409 | "peerDependencies": {
410 | "devtools-protocol": "*"
411 | }
412 | },
413 | "node_modules/cliui": {
414 | "version": "8.0.1",
415 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
416 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
417 | "license": "ISC",
418 | "dependencies": {
419 | "string-width": "^4.2.0",
420 | "strip-ansi": "^6.0.1",
421 | "wrap-ansi": "^7.0.0"
422 | },
423 | "engines": {
424 | "node": ">=12"
425 | }
426 | },
427 | "node_modules/color-convert": {
428 | "version": "2.0.1",
429 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
430 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
431 | "license": "MIT",
432 | "dependencies": {
433 | "color-name": "~1.1.4"
434 | },
435 | "engines": {
436 | "node": ">=7.0.0"
437 | }
438 | },
439 | "node_modules/color-name": {
440 | "version": "1.1.4",
441 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
442 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
443 | "license": "MIT"
444 | },
445 | "node_modules/combined-stream": {
446 | "version": "1.0.8",
447 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
448 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
449 | "license": "MIT",
450 | "dependencies": {
451 | "delayed-stream": "~1.0.0"
452 | },
453 | "engines": {
454 | "node": ">= 0.8"
455 | }
456 | },
457 | "node_modules/cosmiconfig": {
458 | "version": "9.0.0",
459 | "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
460 | "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
461 | "license": "MIT",
462 | "dependencies": {
463 | "env-paths": "^2.2.1",
464 | "import-fresh": "^3.3.0",
465 | "js-yaml": "^4.1.0",
466 | "parse-json": "^5.2.0"
467 | },
468 | "engines": {
469 | "node": ">=14"
470 | },
471 | "funding": {
472 | "url": "https://github.com/sponsors/d-fischer"
473 | },
474 | "peerDependencies": {
475 | "typescript": ">=4.9.5"
476 | },
477 | "peerDependenciesMeta": {
478 | "typescript": {
479 | "optional": true
480 | }
481 | }
482 | },
483 | "node_modules/create-require": {
484 | "version": "1.1.1",
485 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
486 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
487 | "dev": true,
488 | "license": "MIT"
489 | },
490 | "node_modules/data-uri-to-buffer": {
491 | "version": "6.0.2",
492 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
493 | "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
494 | "license": "MIT",
495 | "engines": {
496 | "node": ">= 14"
497 | }
498 | },
499 | "node_modules/debug": {
500 | "version": "4.3.7",
501 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
502 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
503 | "license": "MIT",
504 | "dependencies": {
505 | "ms": "^2.1.3"
506 | },
507 | "engines": {
508 | "node": ">=6.0"
509 | },
510 | "peerDependenciesMeta": {
511 | "supports-color": {
512 | "optional": true
513 | }
514 | }
515 | },
516 | "node_modules/degenerator": {
517 | "version": "5.0.1",
518 | "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
519 | "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
520 | "license": "MIT",
521 | "dependencies": {
522 | "ast-types": "^0.13.4",
523 | "escodegen": "^2.1.0",
524 | "esprima": "^4.0.1"
525 | },
526 | "engines": {
527 | "node": ">= 14"
528 | }
529 | },
530 | "node_modules/delayed-stream": {
531 | "version": "1.0.0",
532 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
533 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
534 | "license": "MIT",
535 | "engines": {
536 | "node": ">=0.4.0"
537 | }
538 | },
539 | "node_modules/devtools-protocol": {
540 | "version": "0.0.1354347",
541 | "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1354347.tgz",
542 | "integrity": "sha512-BlmkSqV0V84E2WnEnoPnwyix57rQxAM5SKJjf4TbYOCGLAWtz8CDH8RIaGOjPgPCXo2Mce3kxSY497OySidY3Q==",
543 | "license": "BSD-3-Clause"
544 | },
545 | "node_modules/diff": {
546 | "version": "4.0.2",
547 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
548 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
549 | "dev": true,
550 | "license": "BSD-3-Clause",
551 | "engines": {
552 | "node": ">=0.3.1"
553 | }
554 | },
555 | "node_modules/emoji-regex": {
556 | "version": "8.0.0",
557 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
558 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
559 | "license": "MIT"
560 | },
561 | "node_modules/end-of-stream": {
562 | "version": "1.4.4",
563 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
564 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
565 | "license": "MIT",
566 | "dependencies": {
567 | "once": "^1.4.0"
568 | }
569 | },
570 | "node_modules/env-paths": {
571 | "version": "2.2.1",
572 | "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
573 | "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
574 | "license": "MIT",
575 | "engines": {
576 | "node": ">=6"
577 | }
578 | },
579 | "node_modules/error-ex": {
580 | "version": "1.3.2",
581 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
582 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
583 | "license": "MIT",
584 | "dependencies": {
585 | "is-arrayish": "^0.2.1"
586 | }
587 | },
588 | "node_modules/escalade": {
589 | "version": "3.2.0",
590 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
591 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
592 | "license": "MIT",
593 | "engines": {
594 | "node": ">=6"
595 | }
596 | },
597 | "node_modules/escodegen": {
598 | "version": "2.1.0",
599 | "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
600 | "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
601 | "license": "BSD-2-Clause",
602 | "dependencies": {
603 | "esprima": "^4.0.1",
604 | "estraverse": "^5.2.0",
605 | "esutils": "^2.0.2"
606 | },
607 | "bin": {
608 | "escodegen": "bin/escodegen.js",
609 | "esgenerate": "bin/esgenerate.js"
610 | },
611 | "engines": {
612 | "node": ">=6.0"
613 | },
614 | "optionalDependencies": {
615 | "source-map": "~0.6.1"
616 | }
617 | },
618 | "node_modules/esprima": {
619 | "version": "4.0.1",
620 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
621 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
622 | "license": "BSD-2-Clause",
623 | "bin": {
624 | "esparse": "bin/esparse.js",
625 | "esvalidate": "bin/esvalidate.js"
626 | },
627 | "engines": {
628 | "node": ">=4"
629 | }
630 | },
631 | "node_modules/estraverse": {
632 | "version": "5.3.0",
633 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
634 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
635 | "license": "BSD-2-Clause",
636 | "engines": {
637 | "node": ">=4.0"
638 | }
639 | },
640 | "node_modules/esutils": {
641 | "version": "2.0.3",
642 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
643 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
644 | "license": "BSD-2-Clause",
645 | "engines": {
646 | "node": ">=0.10.0"
647 | }
648 | },
649 | "node_modules/extract-zip": {
650 | "version": "2.0.1",
651 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
652 | "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
653 | "license": "BSD-2-Clause",
654 | "dependencies": {
655 | "debug": "^4.1.1",
656 | "get-stream": "^5.1.0",
657 | "yauzl": "^2.10.0"
658 | },
659 | "bin": {
660 | "extract-zip": "cli.js"
661 | },
662 | "engines": {
663 | "node": ">= 10.17.0"
664 | },
665 | "optionalDependencies": {
666 | "@types/yauzl": "^2.9.1"
667 | }
668 | },
669 | "node_modules/fast-fifo": {
670 | "version": "1.3.2",
671 | "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
672 | "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
673 | "license": "MIT"
674 | },
675 | "node_modules/fd-slicer": {
676 | "version": "1.1.0",
677 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
678 | "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
679 | "license": "MIT",
680 | "dependencies": {
681 | "pend": "~1.2.0"
682 | }
683 | },
684 | "node_modules/follow-redirects": {
685 | "version": "1.15.9",
686 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
687 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
688 | "funding": [
689 | {
690 | "type": "individual",
691 | "url": "https://github.com/sponsors/RubenVerborgh"
692 | }
693 | ],
694 | "license": "MIT",
695 | "engines": {
696 | "node": ">=4.0"
697 | },
698 | "peerDependenciesMeta": {
699 | "debug": {
700 | "optional": true
701 | }
702 | }
703 | },
704 | "node_modules/form-data": {
705 | "version": "4.0.1",
706 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
707 | "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
708 | "license": "MIT",
709 | "dependencies": {
710 | "asynckit": "^0.4.0",
711 | "combined-stream": "^1.0.8",
712 | "mime-types": "^2.1.12"
713 | },
714 | "engines": {
715 | "node": ">= 6"
716 | }
717 | },
718 | "node_modules/fs-extra": {
719 | "version": "11.2.0",
720 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
721 | "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
722 | "license": "MIT",
723 | "dependencies": {
724 | "graceful-fs": "^4.2.0",
725 | "jsonfile": "^6.0.1",
726 | "universalify": "^2.0.0"
727 | },
728 | "engines": {
729 | "node": ">=14.14"
730 | }
731 | },
732 | "node_modules/get-caller-file": {
733 | "version": "2.0.5",
734 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
735 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
736 | "license": "ISC",
737 | "engines": {
738 | "node": "6.* || 8.* || >= 10.*"
739 | }
740 | },
741 | "node_modules/get-stream": {
742 | "version": "5.2.0",
743 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
744 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
745 | "license": "MIT",
746 | "dependencies": {
747 | "pump": "^3.0.0"
748 | },
749 | "engines": {
750 | "node": ">=8"
751 | },
752 | "funding": {
753 | "url": "https://github.com/sponsors/sindresorhus"
754 | }
755 | },
756 | "node_modules/get-uri": {
757 | "version": "6.0.3",
758 | "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz",
759 | "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==",
760 | "license": "MIT",
761 | "dependencies": {
762 | "basic-ftp": "^5.0.2",
763 | "data-uri-to-buffer": "^6.0.2",
764 | "debug": "^4.3.4",
765 | "fs-extra": "^11.2.0"
766 | },
767 | "engines": {
768 | "node": ">= 14"
769 | }
770 | },
771 | "node_modules/graceful-fs": {
772 | "version": "4.2.11",
773 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
774 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
775 | "license": "ISC"
776 | },
777 | "node_modules/http-proxy-agent": {
778 | "version": "7.0.2",
779 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
780 | "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
781 | "license": "MIT",
782 | "dependencies": {
783 | "agent-base": "^7.1.0",
784 | "debug": "^4.3.4"
785 | },
786 | "engines": {
787 | "node": ">= 14"
788 | }
789 | },
790 | "node_modules/https-proxy-agent": {
791 | "version": "7.0.5",
792 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
793 | "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
794 | "license": "MIT",
795 | "dependencies": {
796 | "agent-base": "^7.0.2",
797 | "debug": "4"
798 | },
799 | "engines": {
800 | "node": ">= 14"
801 | }
802 | },
803 | "node_modules/ieee754": {
804 | "version": "1.2.1",
805 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
806 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
807 | "funding": [
808 | {
809 | "type": "github",
810 | "url": "https://github.com/sponsors/feross"
811 | },
812 | {
813 | "type": "patreon",
814 | "url": "https://www.patreon.com/feross"
815 | },
816 | {
817 | "type": "consulting",
818 | "url": "https://feross.org/support"
819 | }
820 | ],
821 | "license": "BSD-3-Clause"
822 | },
823 | "node_modules/import-fresh": {
824 | "version": "3.3.0",
825 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
826 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
827 | "license": "MIT",
828 | "dependencies": {
829 | "parent-module": "^1.0.0",
830 | "resolve-from": "^4.0.0"
831 | },
832 | "engines": {
833 | "node": ">=6"
834 | },
835 | "funding": {
836 | "url": "https://github.com/sponsors/sindresorhus"
837 | }
838 | },
839 | "node_modules/ip-address": {
840 | "version": "9.0.5",
841 | "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
842 | "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
843 | "license": "MIT",
844 | "dependencies": {
845 | "jsbn": "1.1.0",
846 | "sprintf-js": "^1.1.3"
847 | },
848 | "engines": {
849 | "node": ">= 12"
850 | }
851 | },
852 | "node_modules/is-arrayish": {
853 | "version": "0.2.1",
854 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
855 | "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
856 | "license": "MIT"
857 | },
858 | "node_modules/is-fullwidth-code-point": {
859 | "version": "3.0.0",
860 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
861 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
862 | "license": "MIT",
863 | "engines": {
864 | "node": ">=8"
865 | }
866 | },
867 | "node_modules/js-tokens": {
868 | "version": "4.0.0",
869 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
870 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
871 | "license": "MIT"
872 | },
873 | "node_modules/js-yaml": {
874 | "version": "4.1.0",
875 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
876 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
877 | "license": "MIT",
878 | "dependencies": {
879 | "argparse": "^2.0.1"
880 | },
881 | "bin": {
882 | "js-yaml": "bin/js-yaml.js"
883 | }
884 | },
885 | "node_modules/jsbn": {
886 | "version": "1.1.0",
887 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
888 | "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
889 | "license": "MIT"
890 | },
891 | "node_modules/json-parse-even-better-errors": {
892 | "version": "2.3.1",
893 | "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
894 | "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
895 | "license": "MIT"
896 | },
897 | "node_modules/jsonfile": {
898 | "version": "6.1.0",
899 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
900 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
901 | "license": "MIT",
902 | "dependencies": {
903 | "universalify": "^2.0.0"
904 | },
905 | "optionalDependencies": {
906 | "graceful-fs": "^4.1.6"
907 | }
908 | },
909 | "node_modules/lines-and-columns": {
910 | "version": "1.2.4",
911 | "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
912 | "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
913 | "license": "MIT"
914 | },
915 | "node_modules/lru-cache": {
916 | "version": "7.18.3",
917 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
918 | "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
919 | "license": "ISC",
920 | "engines": {
921 | "node": ">=12"
922 | }
923 | },
924 | "node_modules/make-error": {
925 | "version": "1.3.6",
926 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
927 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
928 | "dev": true,
929 | "license": "ISC"
930 | },
931 | "node_modules/mime-db": {
932 | "version": "1.52.0",
933 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
934 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
935 | "license": "MIT",
936 | "engines": {
937 | "node": ">= 0.6"
938 | }
939 | },
940 | "node_modules/mime-types": {
941 | "version": "2.1.35",
942 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
943 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
944 | "license": "MIT",
945 | "dependencies": {
946 | "mime-db": "1.52.0"
947 | },
948 | "engines": {
949 | "node": ">= 0.6"
950 | }
951 | },
952 | "node_modules/mitt": {
953 | "version": "3.0.1",
954 | "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
955 | "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
956 | "license": "MIT"
957 | },
958 | "node_modules/ms": {
959 | "version": "2.1.3",
960 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
961 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
962 | "license": "MIT"
963 | },
964 | "node_modules/netmask": {
965 | "version": "2.0.2",
966 | "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
967 | "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
968 | "license": "MIT",
969 | "engines": {
970 | "node": ">= 0.4.0"
971 | }
972 | },
973 | "node_modules/once": {
974 | "version": "1.4.0",
975 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
976 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
977 | "license": "ISC",
978 | "dependencies": {
979 | "wrappy": "1"
980 | }
981 | },
982 | "node_modules/pac-proxy-agent": {
983 | "version": "7.0.2",
984 | "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz",
985 | "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==",
986 | "license": "MIT",
987 | "dependencies": {
988 | "@tootallnate/quickjs-emscripten": "^0.23.0",
989 | "agent-base": "^7.0.2",
990 | "debug": "^4.3.4",
991 | "get-uri": "^6.0.1",
992 | "http-proxy-agent": "^7.0.0",
993 | "https-proxy-agent": "^7.0.5",
994 | "pac-resolver": "^7.0.1",
995 | "socks-proxy-agent": "^8.0.4"
996 | },
997 | "engines": {
998 | "node": ">= 14"
999 | }
1000 | },
1001 | "node_modules/pac-resolver": {
1002 | "version": "7.0.1",
1003 | "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
1004 | "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
1005 | "license": "MIT",
1006 | "dependencies": {
1007 | "degenerator": "^5.0.0",
1008 | "netmask": "^2.0.2"
1009 | },
1010 | "engines": {
1011 | "node": ">= 14"
1012 | }
1013 | },
1014 | "node_modules/parent-module": {
1015 | "version": "1.0.1",
1016 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
1017 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
1018 | "license": "MIT",
1019 | "dependencies": {
1020 | "callsites": "^3.0.0"
1021 | },
1022 | "engines": {
1023 | "node": ">=6"
1024 | }
1025 | },
1026 | "node_modules/parse-json": {
1027 | "version": "5.2.0",
1028 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
1029 | "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
1030 | "license": "MIT",
1031 | "dependencies": {
1032 | "@babel/code-frame": "^7.0.0",
1033 | "error-ex": "^1.3.1",
1034 | "json-parse-even-better-errors": "^2.3.0",
1035 | "lines-and-columns": "^1.1.6"
1036 | },
1037 | "engines": {
1038 | "node": ">=8"
1039 | },
1040 | "funding": {
1041 | "url": "https://github.com/sponsors/sindresorhus"
1042 | }
1043 | },
1044 | "node_modules/pend": {
1045 | "version": "1.2.0",
1046 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
1047 | "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
1048 | "license": "MIT"
1049 | },
1050 | "node_modules/picocolors": {
1051 | "version": "1.1.1",
1052 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1053 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1054 | "license": "ISC"
1055 | },
1056 | "node_modules/progress": {
1057 | "version": "2.0.3",
1058 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
1059 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
1060 | "license": "MIT",
1061 | "engines": {
1062 | "node": ">=0.4.0"
1063 | }
1064 | },
1065 | "node_modules/proxy-agent": {
1066 | "version": "6.4.0",
1067 | "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
1068 | "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",
1069 | "license": "MIT",
1070 | "dependencies": {
1071 | "agent-base": "^7.0.2",
1072 | "debug": "^4.3.4",
1073 | "http-proxy-agent": "^7.0.1",
1074 | "https-proxy-agent": "^7.0.3",
1075 | "lru-cache": "^7.14.1",
1076 | "pac-proxy-agent": "^7.0.1",
1077 | "proxy-from-env": "^1.1.0",
1078 | "socks-proxy-agent": "^8.0.2"
1079 | },
1080 | "engines": {
1081 | "node": ">= 14"
1082 | }
1083 | },
1084 | "node_modules/proxy-from-env": {
1085 | "version": "1.1.0",
1086 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1087 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
1088 | "license": "MIT"
1089 | },
1090 | "node_modules/pump": {
1091 | "version": "3.0.2",
1092 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
1093 | "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
1094 | "license": "MIT",
1095 | "dependencies": {
1096 | "end-of-stream": "^1.1.0",
1097 | "once": "^1.3.1"
1098 | }
1099 | },
1100 | "node_modules/puppeteer": {
1101 | "version": "23.6.1",
1102 | "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.6.1.tgz",
1103 | "integrity": "sha512-8+ALGQgwXd3P/tGcuSsxTPGDaOQIjcDIm04I5hpWZv/PiN5q8bQNHRUyfYrifT+flnM9aTWCP7tLEzuB6SlIgA==",
1104 | "hasInstallScript": true,
1105 | "license": "Apache-2.0",
1106 | "dependencies": {
1107 | "@puppeteer/browsers": "2.4.0",
1108 | "chromium-bidi": "0.8.0",
1109 | "cosmiconfig": "^9.0.0",
1110 | "devtools-protocol": "0.0.1354347",
1111 | "puppeteer-core": "23.6.1",
1112 | "typed-query-selector": "^2.12.0"
1113 | },
1114 | "bin": {
1115 | "puppeteer": "lib/cjs/puppeteer/node/cli.js"
1116 | },
1117 | "engines": {
1118 | "node": ">=18"
1119 | }
1120 | },
1121 | "node_modules/puppeteer-core": {
1122 | "version": "23.6.1",
1123 | "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.6.1.tgz",
1124 | "integrity": "sha512-DoNLAzQfGklPauEn33N4h9cM9GubJSINEn+AUMwAXwW159Y9JLk5y34Jsbv4c7kG8P0puOYWV9leu2siMZ/QpQ==",
1125 | "license": "Apache-2.0",
1126 | "dependencies": {
1127 | "@puppeteer/browsers": "2.4.0",
1128 | "chromium-bidi": "0.8.0",
1129 | "debug": "^4.3.7",
1130 | "devtools-protocol": "0.0.1354347",
1131 | "typed-query-selector": "^2.12.0",
1132 | "ws": "^8.18.0"
1133 | },
1134 | "engines": {
1135 | "node": ">=18"
1136 | }
1137 | },
1138 | "node_modules/queue-tick": {
1139 | "version": "1.0.1",
1140 | "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
1141 | "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
1142 | "license": "MIT"
1143 | },
1144 | "node_modules/require-directory": {
1145 | "version": "2.1.1",
1146 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
1147 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
1148 | "license": "MIT",
1149 | "engines": {
1150 | "node": ">=0.10.0"
1151 | }
1152 | },
1153 | "node_modules/resolve-from": {
1154 | "version": "4.0.0",
1155 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
1156 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
1157 | "license": "MIT",
1158 | "engines": {
1159 | "node": ">=4"
1160 | }
1161 | },
1162 | "node_modules/semver": {
1163 | "version": "7.6.3",
1164 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
1165 | "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
1166 | "license": "ISC",
1167 | "bin": {
1168 | "semver": "bin/semver.js"
1169 | },
1170 | "engines": {
1171 | "node": ">=10"
1172 | }
1173 | },
1174 | "node_modules/smart-buffer": {
1175 | "version": "4.2.0",
1176 | "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
1177 | "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
1178 | "license": "MIT",
1179 | "engines": {
1180 | "node": ">= 6.0.0",
1181 | "npm": ">= 3.0.0"
1182 | }
1183 | },
1184 | "node_modules/socks": {
1185 | "version": "2.8.3",
1186 | "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
1187 | "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
1188 | "license": "MIT",
1189 | "dependencies": {
1190 | "ip-address": "^9.0.5",
1191 | "smart-buffer": "^4.2.0"
1192 | },
1193 | "engines": {
1194 | "node": ">= 10.0.0",
1195 | "npm": ">= 3.0.0"
1196 | }
1197 | },
1198 | "node_modules/socks-proxy-agent": {
1199 | "version": "8.0.4",
1200 | "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
1201 | "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
1202 | "license": "MIT",
1203 | "dependencies": {
1204 | "agent-base": "^7.1.1",
1205 | "debug": "^4.3.4",
1206 | "socks": "^2.8.3"
1207 | },
1208 | "engines": {
1209 | "node": ">= 14"
1210 | }
1211 | },
1212 | "node_modules/source-map": {
1213 | "version": "0.6.1",
1214 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
1215 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
1216 | "license": "BSD-3-Clause",
1217 | "optional": true,
1218 | "engines": {
1219 | "node": ">=0.10.0"
1220 | }
1221 | },
1222 | "node_modules/sprintf-js": {
1223 | "version": "1.1.3",
1224 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
1225 | "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
1226 | "license": "BSD-3-Clause"
1227 | },
1228 | "node_modules/streamx": {
1229 | "version": "2.20.1",
1230 | "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz",
1231 | "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==",
1232 | "license": "MIT",
1233 | "dependencies": {
1234 | "fast-fifo": "^1.3.2",
1235 | "queue-tick": "^1.0.1",
1236 | "text-decoder": "^1.1.0"
1237 | },
1238 | "optionalDependencies": {
1239 | "bare-events": "^2.2.0"
1240 | }
1241 | },
1242 | "node_modules/string-width": {
1243 | "version": "4.2.3",
1244 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
1245 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
1246 | "license": "MIT",
1247 | "dependencies": {
1248 | "emoji-regex": "^8.0.0",
1249 | "is-fullwidth-code-point": "^3.0.0",
1250 | "strip-ansi": "^6.0.1"
1251 | },
1252 | "engines": {
1253 | "node": ">=8"
1254 | }
1255 | },
1256 | "node_modules/strip-ansi": {
1257 | "version": "6.0.1",
1258 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
1259 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
1260 | "license": "MIT",
1261 | "dependencies": {
1262 | "ansi-regex": "^5.0.1"
1263 | },
1264 | "engines": {
1265 | "node": ">=8"
1266 | }
1267 | },
1268 | "node_modules/tar-fs": {
1269 | "version": "3.0.6",
1270 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
1271 | "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
1272 | "license": "MIT",
1273 | "dependencies": {
1274 | "pump": "^3.0.0",
1275 | "tar-stream": "^3.1.5"
1276 | },
1277 | "optionalDependencies": {
1278 | "bare-fs": "^2.1.1",
1279 | "bare-path": "^2.1.0"
1280 | }
1281 | },
1282 | "node_modules/tar-stream": {
1283 | "version": "3.1.7",
1284 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
1285 | "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
1286 | "license": "MIT",
1287 | "dependencies": {
1288 | "b4a": "^1.6.4",
1289 | "fast-fifo": "^1.2.0",
1290 | "streamx": "^2.15.0"
1291 | }
1292 | },
1293 | "node_modules/text-decoder": {
1294 | "version": "1.2.1",
1295 | "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz",
1296 | "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==",
1297 | "license": "Apache-2.0"
1298 | },
1299 | "node_modules/through": {
1300 | "version": "2.3.8",
1301 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
1302 | "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
1303 | "license": "MIT"
1304 | },
1305 | "node_modules/ts-node": {
1306 | "version": "10.9.2",
1307 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
1308 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
1309 | "dev": true,
1310 | "license": "MIT",
1311 | "dependencies": {
1312 | "@cspotcode/source-map-support": "^0.8.0",
1313 | "@tsconfig/node10": "^1.0.7",
1314 | "@tsconfig/node12": "^1.0.7",
1315 | "@tsconfig/node14": "^1.0.0",
1316 | "@tsconfig/node16": "^1.0.2",
1317 | "acorn": "^8.4.1",
1318 | "acorn-walk": "^8.1.1",
1319 | "arg": "^4.1.0",
1320 | "create-require": "^1.1.0",
1321 | "diff": "^4.0.1",
1322 | "make-error": "^1.1.1",
1323 | "v8-compile-cache-lib": "^3.0.1",
1324 | "yn": "3.1.1"
1325 | },
1326 | "bin": {
1327 | "ts-node": "dist/bin.js",
1328 | "ts-node-cwd": "dist/bin-cwd.js",
1329 | "ts-node-esm": "dist/bin-esm.js",
1330 | "ts-node-script": "dist/bin-script.js",
1331 | "ts-node-transpile-only": "dist/bin-transpile.js",
1332 | "ts-script": "dist/bin-script-deprecated.js"
1333 | },
1334 | "peerDependencies": {
1335 | "@swc/core": ">=1.2.50",
1336 | "@swc/wasm": ">=1.2.50",
1337 | "@types/node": "*",
1338 | "typescript": ">=2.7"
1339 | },
1340 | "peerDependenciesMeta": {
1341 | "@swc/core": {
1342 | "optional": true
1343 | },
1344 | "@swc/wasm": {
1345 | "optional": true
1346 | }
1347 | }
1348 | },
1349 | "node_modules/tslib": {
1350 | "version": "2.8.1",
1351 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1352 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1353 | "license": "0BSD"
1354 | },
1355 | "node_modules/typed-query-selector": {
1356 | "version": "2.12.0",
1357 | "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
1358 | "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
1359 | "license": "MIT"
1360 | },
1361 | "node_modules/typescript": {
1362 | "version": "5.6.3",
1363 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
1364 | "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
1365 | "devOptional": true,
1366 | "license": "Apache-2.0",
1367 | "bin": {
1368 | "tsc": "bin/tsc",
1369 | "tsserver": "bin/tsserver"
1370 | },
1371 | "engines": {
1372 | "node": ">=14.17"
1373 | }
1374 | },
1375 | "node_modules/unbzip2-stream": {
1376 | "version": "1.4.3",
1377 | "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
1378 | "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
1379 | "license": "MIT",
1380 | "dependencies": {
1381 | "buffer": "^5.2.1",
1382 | "through": "^2.3.8"
1383 | }
1384 | },
1385 | "node_modules/undici-types": {
1386 | "version": "6.19.8",
1387 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
1388 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
1389 | "devOptional": true,
1390 | "license": "MIT"
1391 | },
1392 | "node_modules/universalify": {
1393 | "version": "2.0.1",
1394 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
1395 | "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
1396 | "license": "MIT",
1397 | "engines": {
1398 | "node": ">= 10.0.0"
1399 | }
1400 | },
1401 | "node_modules/urlpattern-polyfill": {
1402 | "version": "10.0.0",
1403 | "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
1404 | "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==",
1405 | "license": "MIT"
1406 | },
1407 | "node_modules/v8-compile-cache-lib": {
1408 | "version": "3.0.1",
1409 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
1410 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
1411 | "dev": true,
1412 | "license": "MIT"
1413 | },
1414 | "node_modules/wrap-ansi": {
1415 | "version": "7.0.0",
1416 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
1417 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
1418 | "license": "MIT",
1419 | "dependencies": {
1420 | "ansi-styles": "^4.0.0",
1421 | "string-width": "^4.1.0",
1422 | "strip-ansi": "^6.0.0"
1423 | },
1424 | "engines": {
1425 | "node": ">=10"
1426 | },
1427 | "funding": {
1428 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1429 | }
1430 | },
1431 | "node_modules/wrappy": {
1432 | "version": "1.0.2",
1433 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1434 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
1435 | "license": "ISC"
1436 | },
1437 | "node_modules/ws": {
1438 | "version": "8.18.0",
1439 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
1440 | "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
1441 | "license": "MIT",
1442 | "engines": {
1443 | "node": ">=10.0.0"
1444 | },
1445 | "peerDependencies": {
1446 | "bufferutil": "^4.0.1",
1447 | "utf-8-validate": ">=5.0.2"
1448 | },
1449 | "peerDependenciesMeta": {
1450 | "bufferutil": {
1451 | "optional": true
1452 | },
1453 | "utf-8-validate": {
1454 | "optional": true
1455 | }
1456 | }
1457 | },
1458 | "node_modules/y18n": {
1459 | "version": "5.0.8",
1460 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
1461 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
1462 | "license": "ISC",
1463 | "engines": {
1464 | "node": ">=10"
1465 | }
1466 | },
1467 | "node_modules/yargs": {
1468 | "version": "17.7.2",
1469 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
1470 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
1471 | "license": "MIT",
1472 | "dependencies": {
1473 | "cliui": "^8.0.1",
1474 | "escalade": "^3.1.1",
1475 | "get-caller-file": "^2.0.5",
1476 | "require-directory": "^2.1.1",
1477 | "string-width": "^4.2.3",
1478 | "y18n": "^5.0.5",
1479 | "yargs-parser": "^21.1.1"
1480 | },
1481 | "engines": {
1482 | "node": ">=12"
1483 | }
1484 | },
1485 | "node_modules/yargs-parser": {
1486 | "version": "21.1.1",
1487 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
1488 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
1489 | "license": "ISC",
1490 | "engines": {
1491 | "node": ">=12"
1492 | }
1493 | },
1494 | "node_modules/yauzl": {
1495 | "version": "2.10.0",
1496 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
1497 | "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
1498 | "license": "MIT",
1499 | "dependencies": {
1500 | "buffer-crc32": "~0.2.3",
1501 | "fd-slicer": "~1.1.0"
1502 | }
1503 | },
1504 | "node_modules/yn": {
1505 | "version": "3.1.1",
1506 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
1507 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
1508 | "dev": true,
1509 | "license": "MIT",
1510 | "engines": {
1511 | "node": ">=6"
1512 | }
1513 | },
1514 | "node_modules/zod": {
1515 | "version": "3.23.8",
1516 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
1517 | "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
1518 | "license": "MIT",
1519 | "funding": {
1520 | "url": "https://github.com/sponsors/colinhacks"
1521 | }
1522 | }
1523 | }
1524 | }
1525 |
--------------------------------------------------------------------------------