├── 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 | Scribe Banner 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 | 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 | --------------------------------------------------------------------------------