From b693369faed54606bb5bac8b3d2a59631f6a198c Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Fri, 3 Apr 2026 11:06:16 -0600 Subject: [PATCH 1/3] feat: add human-like smooth typing with optional typo injection Adds smooth typing mode to POST /computer/type that types text in word-sized chunks with variable intra-word delays and natural inter-word pauses via xdotool, following the same Go-side sleep pattern as doMoveMouseSmooth. Optionally injects realistic typos (adjacent-key, doubling, transpose) using geometric gap sampling (O(typos) random calls, not O(chars)) with QWERTY adjacency lookup, then corrects them with backspace after a "realization" pause. New API fields on TypeTextRequest: - smooth: boolean (default false) - enable human-like timing - typo_chance: number 0.0-0.10 (default 0) - per-char typo rate New package: server/lib/typinghumanizer with word chunking, QWERTY adjacency map, and typo position generation. Made-with: Cursor --- server/cmd/api/api/computer.go | 179 ++++- server/go.sum | 5 +- server/lib/oapi/oapi.go | 630 +++++++----------- server/lib/typinghumanizer/typinghumanizer.go | 167 +++++ .../typinghumanizer/typinghumanizer_test.go | 192 ++++++ server/openapi.yaml | 19 +- 6 files changed, 780 insertions(+), 412 deletions(-) create mode 100644 server/lib/typinghumanizer/typinghumanizer.go create mode 100644 server/lib/typinghumanizer/typinghumanizer_test.go diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index 2367cd68..b5f00dde 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -18,6 +18,7 @@ import ( "github.com/onkernel/kernel-images/server/lib/logger" "github.com/onkernel/kernel-images/server/lib/mousetrajectory" + "github.com/onkernel/kernel-images/server/lib/typinghumanizer" oapi "github.com/onkernel/kernel-images/server/lib/oapi" ) @@ -450,19 +451,20 @@ func (s *ApiService) TakeScreenshot(ctx context.Context, request oapi.TakeScreen } func (s *ApiService) doTypeText(ctx context.Context, body oapi.TypeTextRequest) error { + if body.Smooth != nil && *body.Smooth { + return s.doTypeTextSmooth(ctx, body) + } + log := logger.FromContext(ctx) - // Validate delay if provided if body.Delay != nil && *body.Delay < 0 { return &validationError{msg: "delay must be >= 0 milliseconds"} } - // Build xdotool arguments args := []string{"type"} if body.Delay != nil { args = append(args, "--delay", strconv.Itoa(*body.Delay)) } - // Use "--" to terminate options and pass raw text args = append(args, "--", body.Text) output, err := defaultXdoTool.Run(ctx, args...) @@ -474,6 +476,177 @@ func (s *ApiService) doTypeText(ctx context.Context, body oapi.TypeTextRequest) return nil } +func (s *ApiService) doTypeTextSmooth(ctx context.Context, body oapi.TypeTextRequest) error { + log := logger.FromContext(ctx) + + if body.TypoChance != nil && (*body.TypoChance < 0 || *body.TypoChance > 0.10) { + return &validationError{msg: "typo_chance must be between 0.0 and 0.10"} + } + + rng := rand.New(rand.NewSource(rand.Int63())) + runes := []rune(body.Text) + + var typoRate float64 + if body.TypoChance != nil { + typoRate = float64(*body.TypoChance) + } + typos := typinghumanizer.GenerateTypoPositions(rng, len(runes), typoRate) + + // Build a typo lookup set for O(1) access during chunk iteration + typoByPos := map[int]typinghumanizer.Typo{} + for _, typo := range typos { + typoByPos[typo.Pos] = typo + } + + chunks := typinghumanizer.SplitWordChunks(body.Text) + if len(chunks) == 0 { + return nil + } + + globalPos := 0 + for chunkIdx, chunk := range chunks { + select { + case <-ctx.Done(): + return &executionError{msg: "typing cancelled"} + default: + } + + chunkRunes := []rune(chunk) + chunkStart := globalPos + chunkEnd := chunkStart + len(chunkRunes) + + // Find typos within this chunk + var chunkTypo *typinghumanizer.Typo + for pos := chunkStart; pos < chunkEnd; pos++ { + if t, ok := typoByPos[pos]; ok { + chunkTypo = &t + break + } + } + + intraDelayMs := rng.Intn(70) + 50 + + if chunkTypo == nil { + if err := s.xdotoolTypeChunk(ctx, chunk, intraDelayMs); err != nil { + log.Error("xdotool type chunk failed", "err", err, "chunk", chunkIdx) + return &executionError{msg: "failed during smooth typing"} + } + } else { + localPos := chunkTypo.Pos - chunkStart + if err := s.typeChunkWithTypo(ctx, log, rng, chunkRunes, localPos, *chunkTypo, intraDelayMs); err != nil { + return err + } + } + + globalPos = chunkEnd + + if chunkIdx < len(chunks)-1 { + pause := typinghumanizer.UniformJitter(rng, 140, 60, 60) + if typinghumanizer.IsSentenceEnd(chunk) { + pause = pause * 3 / 2 + } + if err := sleepWithContext(ctx, pause); err != nil { + return &executionError{msg: "typing cancelled"} + } + } + } + + log.Info("executed smooth typing", "chunks", len(chunks), "typos", len(typos), "textLen", len(body.Text)) + return nil +} + +func (s *ApiService) xdotoolTypeChunk(ctx context.Context, text string, delayMs int) error { + args := []string{"type", "--delay", strconv.Itoa(delayMs), "--", text} + output, err := defaultXdoTool.Run(ctx, args...) + if err != nil { + return fmt.Errorf("xdotool type failed: %s (output: %s)", err, string(output)) + } + return nil +} + +func (s *ApiService) typeChunkWithTypo( + ctx context.Context, + log *slog.Logger, + rng *rand.Rand, + chunkRunes []rune, + typoLocalPos int, + typo typinghumanizer.Typo, + delayMs int, +) error { + // Type text before the typo + if typoLocalPos > 0 { + before := string(chunkRunes[:typoLocalPos]) + if err := s.xdotoolTypeChunk(ctx, before, delayMs); err != nil { + return &executionError{msg: "failed during smooth typing"} + } + } + + correctChar := chunkRunes[typoLocalPos] + var wrongText string + var backspaces int + + switch typo.Kind { + case typinghumanizer.TypoAdjacentKey: + wrongText = string(typinghumanizer.AdjacentKey(rng, correctChar)) + backspaces = 1 + case typinghumanizer.TypoDoubling: + wrongText = string([]rune{correctChar, correctChar}) + backspaces = 2 + case typinghumanizer.TypoTranspose: + if typoLocalPos+1 < len(chunkRunes) { + wrongText = string([]rune{chunkRunes[typoLocalPos+1], correctChar}) + backspaces = 2 + } else { + wrongText = string(typinghumanizer.AdjacentKey(rng, correctChar)) + backspaces = 1 + } + case typinghumanizer.TypoExtraChar: + wrongText = string([]rune{typinghumanizer.AdjacentKey(rng, correctChar), correctChar}) + backspaces = 2 + } + + // Type the wrong text + if err := s.xdotoolTypeChunk(ctx, wrongText, delayMs); err != nil { + return &executionError{msg: "failed during smooth typing"} + } + + // "Oh no" realization pause + realizationPause := typinghumanizer.UniformJitter(rng, 350, 150, 150) + if err := sleepWithContext(ctx, realizationPause); err != nil { + return &executionError{msg: "typing cancelled"} + } + + // Backspace to correct + bsArgs := make([]string, 0, backspaces*2) + for i := 0; i < backspaces; i++ { + bsArgs = append(bsArgs, "key", "BackSpace") + } + if output, err := defaultXdoTool.Run(ctx, bsArgs...); err != nil { + log.Error("xdotool backspace failed", "err", err, "output", string(output)) + return &executionError{msg: "failed during typo correction"} + } + + // Brief recovery pause + recoveryPause := typinghumanizer.UniformJitter(rng, 80, 30, 40) + if err := sleepWithContext(ctx, recoveryPause); err != nil { + return &executionError{msg: "typing cancelled"} + } + + // Type the correct remainder of the chunk from the typo position onward + var correctText string + if typo.Kind == typinghumanizer.TypoTranspose && typoLocalPos+1 < len(chunkRunes) { + correctText = string(chunkRunes[typoLocalPos:]) + } else { + correctText = string(chunkRunes[typoLocalPos:]) + } + + if err := s.xdotoolTypeChunk(ctx, correctText, delayMs); err != nil { + return &executionError{msg: "failed during smooth typing"} + } + + return nil +} + func (s *ApiService) TypeText(ctx context.Context, request oapi.TypeTextRequestObject) (oapi.TypeTextResponseObject, error) { s.inputMu.Lock() defer s.inputMu.Unlock() diff --git a/server/go.sum b/server/go.sum index baa1fd2c..d13d59b4 100644 --- a/server/go.sum +++ b/server/go.sum @@ -127,8 +127,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/nrednav/cuid2 v1.1.0 h1:Y2P9Fo1Iz7lKuwcn+fS0mbxkNvEqoNLUtm0+moHCnYc= github.com/nrednav/cuid2 v1.1.0/go.mod h1:jBjkJAI+QLM4EUGvtwGDHC1cP1QQrRNfLo/A7qJFDhA= -github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= -github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= @@ -175,9 +173,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 4854c577..127d1ea5 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -1,6 +1,6 @@ // Package oapi provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. package oapi import ( @@ -9,7 +9,6 @@ import ( "context" "encoding/base64" "encoding/json" - "errors" "fmt" "io" "mime/multipart" @@ -35,24 +34,6 @@ const ( ClickMouseRequestButtonRight ClickMouseRequestButton = "right" ) -// Valid indicates whether the value is a known member of the ClickMouseRequestButton enum. -func (e ClickMouseRequestButton) Valid() bool { - switch e { - case ClickMouseRequestButtonBack: - return true - case ClickMouseRequestButtonForward: - return true - case ClickMouseRequestButtonLeft: - return true - case ClickMouseRequestButtonMiddle: - return true - case ClickMouseRequestButtonRight: - return true - default: - return false - } -} - // Defines values for ClickMouseRequestClickType. const ( Click ClickMouseRequestClickType = "click" @@ -60,20 +41,6 @@ const ( Up ClickMouseRequestClickType = "up" ) -// Valid indicates whether the value is a known member of the ClickMouseRequestClickType enum. -func (e ClickMouseRequestClickType) Valid() bool { - switch e { - case Click: - return true - case Down: - return true - case Up: - return true - default: - return false - } -} - // Defines values for ComputerActionType. const ( ClickMouse ComputerActionType = "click_mouse" @@ -86,30 +53,6 @@ const ( TypeText ComputerActionType = "type_text" ) -// Valid indicates whether the value is a known member of the ComputerActionType enum. -func (e ComputerActionType) Valid() bool { - switch e { - case ClickMouse: - return true - case DragMouse: - return true - case MoveMouse: - return true - case PressKey: - return true - case Scroll: - return true - case SetCursor: - return true - case Sleep: - return true - case TypeText: - return true - default: - return false - } -} - // Defines values for DragMouseRequestButton. const ( DragMouseRequestButtonLeft DragMouseRequestButton = "left" @@ -117,20 +60,6 @@ const ( DragMouseRequestButtonRight DragMouseRequestButton = "right" ) -// Valid indicates whether the value is a known member of the DragMouseRequestButton enum. -func (e DragMouseRequestButton) Valid() bool { - switch e { - case DragMouseRequestButtonLeft: - return true - case DragMouseRequestButtonMiddle: - return true - case DragMouseRequestButtonRight: - return true - default: - return false - } -} - // Defines values for FileSystemEventType. const ( CREATE FileSystemEventType = "CREATE" @@ -139,22 +68,6 @@ const ( WRITE FileSystemEventType = "WRITE" ) -// Valid indicates whether the value is a known member of the FileSystemEventType enum. -func (e FileSystemEventType) Valid() bool { - switch e { - case CREATE: - return true - case DELETE: - return true - case RENAME: - return true - case WRITE: - return true - default: - return false - } -} - // Defines values for PatchDisplayRequestRefreshRate. const ( N10 PatchDisplayRequestRefreshRate = 10 @@ -163,22 +76,6 @@ const ( N60 PatchDisplayRequestRefreshRate = 60 ) -// Valid indicates whether the value is a known member of the PatchDisplayRequestRefreshRate enum. -func (e PatchDisplayRequestRefreshRate) Valid() bool { - switch e { - case N10: - return true - case N25: - return true - case N30: - return true - case N60: - return true - default: - return false - } -} - // Defines values for ProcessKillRequestSignal. const ( HUP ProcessKillRequestSignal = "HUP" @@ -187,73 +84,23 @@ const ( TERM ProcessKillRequestSignal = "TERM" ) -// Valid indicates whether the value is a known member of the ProcessKillRequestSignal enum. -func (e ProcessKillRequestSignal) Valid() bool { - switch e { - case HUP: - return true - case INT: - return true - case KILL: - return true - case TERM: - return true - default: - return false - } -} - // Defines values for ProcessStatusState. const ( Exited ProcessStatusState = "exited" Running ProcessStatusState = "running" ) -// Valid indicates whether the value is a known member of the ProcessStatusState enum. -func (e ProcessStatusState) Valid() bool { - switch e { - case Exited: - return true - case Running: - return true - default: - return false - } -} - // Defines values for ProcessStreamEventEvent. const ( Exit ProcessStreamEventEvent = "exit" ) -// Valid indicates whether the value is a known member of the ProcessStreamEventEvent enum. -func (e ProcessStreamEventEvent) Valid() bool { - switch e { - case Exit: - return true - default: - return false - } -} - // Defines values for ProcessStreamEventStream. const ( Stderr ProcessStreamEventStream = "stderr" Stdout ProcessStreamEventStream = "stdout" ) -// Valid indicates whether the value is a known member of the ProcessStreamEventStream enum. -func (e ProcessStreamEventStream) Valid() bool { - switch e { - case Stderr: - return true - case Stdout: - return true - default: - return false - } -} - // Defines values for DownloadDirZstdParamsCompressionLevel. const ( Best DownloadDirZstdParamsCompressionLevel = "best" @@ -262,40 +109,12 @@ const ( Fastest DownloadDirZstdParamsCompressionLevel = "fastest" ) -// Valid indicates whether the value is a known member of the DownloadDirZstdParamsCompressionLevel enum. -func (e DownloadDirZstdParamsCompressionLevel) Valid() bool { - switch e { - case Best: - return true - case Better: - return true - case Default: - return true - case Fastest: - return true - default: - return false - } -} - // Defines values for LogsStreamParamsSource. const ( Path LogsStreamParamsSource = "path" Supervisor LogsStreamParamsSource = "supervisor" ) -// Valid indicates whether the value is a known member of the LogsStreamParamsSource enum. -func (e LogsStreamParamsSource) Valid() bool { - switch e { - case Path: - return true - case Supervisor: - return true - default: - return false - } -} - // BatchComputerActionRequest A batch of computer actions to execute sequentially. type BatchComputerActionRequest struct { // Actions Ordered list of actions to execute. Execution stops on the first error. @@ -590,19 +409,19 @@ type ProcessExecRequest struct { AsRoot *bool `json:"as_root,omitempty"` // AsUser Run the process as this user. - AsUser *string `json:"as_user,omitempty"` + AsUser *string `json:"as_user"` // Command Executable or shell command to run. Command string `json:"command"` // Cwd Working directory (absolute path) to run the command in. - Cwd *string `json:"cwd,omitempty"` + Cwd *string `json:"cwd"` // Env Environment variables to set for the process. Env *map[string]string `json:"env,omitempty"` // TimeoutSec Maximum execution time in seconds. - TimeoutSec *int `json:"timeout_sec,omitempty"` + TimeoutSec *int `json:"timeout_sec"` } // ProcessExecResult Result of a synchronous command execution. @@ -650,7 +469,7 @@ type ProcessSpawnRequest struct { AsRoot *bool `json:"as_root,omitempty"` // AsUser Run the process as this user. - AsUser *string `json:"as_user,omitempty"` + AsUser *string `json:"as_user"` // Cols Initial terminal columns when allocate_tty is true. Cols *int `json:"cols,omitempty"` @@ -659,7 +478,7 @@ type ProcessSpawnRequest struct { Command string `json:"command"` // Cwd Working directory (absolute path) to run the command in. - Cwd *string `json:"cwd,omitempty"` + Cwd *string `json:"cwd"` // Env Environment variables to set for the process. Env *map[string]string `json:"env,omitempty"` @@ -668,7 +487,7 @@ type ProcessSpawnRequest struct { Rows *int `json:"rows,omitempty"` // TimeoutSec Maximum execution time in seconds. - TimeoutSec *int `json:"timeout_sec,omitempty"` + TimeoutSec *int `json:"timeout_sec"` } // ProcessSpawnResult Information about a spawned process. @@ -689,7 +508,7 @@ type ProcessStatus struct { CpuPct *float32 `json:"cpu_pct,omitempty"` // ExitCode Exit code if the process has exited. - ExitCode *int `json:"exit_code,omitempty"` + ExitCode *int `json:"exit_code"` // MemBytes Estimated resident memory usage in bytes. MemBytes *int `json:"mem_bytes,omitempty"` @@ -737,12 +556,12 @@ type ProcessStreamEventStream string // RecorderInfo defines model for RecorderInfo. type RecorderInfo struct { // FinishedAt Timestamp when recording finished - FinishedAt *time.Time `json:"finished_at,omitempty"` + FinishedAt *time.Time `json:"finished_at"` Id string `json:"id"` IsRecording bool `json:"isRecording"` // StartedAt Timestamp when recording started - StartedAt *time.Time `json:"started_at,omitempty"` + StartedAt *time.Time `json:"started_at"` } // ScreenshotRegion defines model for ScreenshotRegion. @@ -845,11 +664,22 @@ type StopRecordingRequest struct { // TypeTextRequest defines model for TypeTextRequest. type TypeTextRequest struct { - // Delay Delay in milliseconds between keystrokes + // Delay Delay in milliseconds between keystrokes. Ignored when smooth is true. Delay *int `json:"delay,omitempty"` + // Smooth Use human-like variable keystroke timing instead of a fixed delay. + // When enabled, text is typed in word-sized chunks with variable + // intra-word delays and natural inter-word pauses. The delay field + // is ignored when smooth is true. + Smooth *bool `json:"smooth,omitempty"` + // Text Text to type on the host computer Text string `json:"text"` + + // TypoChance Probability (0.0-0.10) of a typo per character, which is then + // corrected with backspace. Requires smooth to be true. Set to 0 + // to disable. Typical human range is 0.02-0.05. + TypoChance *float32 `json:"typo_chance,omitempty"` } // WriteClipboardRequest defines model for WriteClipboardRequest. @@ -3001,7 +2831,7 @@ func NewDownloadDirZipRequest(server string, params *DownloadDirZipParams) (*htt if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3046,7 +2876,7 @@ func NewDownloadDirZstdRequest(server string, params *DownloadDirZstdParams) (*h if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3060,7 +2890,7 @@ func NewDownloadDirZstdRequest(server string, params *DownloadDirZstdParams) (*h if params.CompressionLevel != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "compression_level", *params.CompressionLevel, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "compression_level", runtime.ParamLocationQuery, *params.CompressionLevel); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3107,7 +2937,7 @@ func NewFileInfoRequest(server string, params *FileInfoParams) (*http.Request, e if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3152,7 +2982,7 @@ func NewListFilesRequest(server string, params *ListFilesParams) (*http.Request, if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3237,7 +3067,7 @@ func NewReadFileRequest(server string, params *ReadFileParams) (*http.Request, e if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3433,7 +3263,7 @@ func NewStopFsWatchRequest(server string, watchId string) (*http.Request, error) var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "watch_id", watchId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "watch_id", runtime.ParamLocationPath, watchId) if err != nil { return nil, err } @@ -3467,7 +3297,7 @@ func NewStreamFsEventsRequest(server string, watchId string) (*http.Request, err var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "watch_id", watchId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "watch_id", runtime.ParamLocationPath, watchId) if err != nil { return nil, err } @@ -3517,7 +3347,7 @@ func NewWriteFileRequestWithBody(server string, params *WriteFileParams, content if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3531,7 +3361,7 @@ func NewWriteFileRequestWithBody(server string, params *WriteFileParams, content if params.Mode != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "mode", *params.Mode, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "mode", runtime.ParamLocationQuery, *params.Mode); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3580,7 +3410,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "source", params.Source, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "source", runtime.ParamLocationQuery, params.Source); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3594,7 +3424,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params.Follow != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "follow", *params.Follow, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "follow", runtime.ParamLocationQuery, *params.Follow); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3610,7 +3440,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params.Path != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", *params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, *params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3626,7 +3456,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params.SupervisorProcess != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "supervisor_process", *params.SupervisorProcess, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "supervisor_process", runtime.ParamLocationQuery, *params.SupervisorProcess); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3788,7 +3618,7 @@ func NewProcessKillRequestWithBody(server string, processId openapi_types.UUID, var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) if err != nil { return nil, err } @@ -3835,7 +3665,7 @@ func NewProcessResizeRequestWithBody(server string, processId openapi_types.UUID var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) if err != nil { return nil, err } @@ -3871,7 +3701,7 @@ func NewProcessStatusRequest(server string, processId openapi_types.UUID) (*http var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) if err != nil { return nil, err } @@ -3916,7 +3746,7 @@ func NewProcessStdinRequestWithBody(server string, processId openapi_types.UUID, var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) if err != nil { return nil, err } @@ -3952,7 +3782,7 @@ func NewProcessStdoutStreamRequest(server string, processId openapi_types.UUID) var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) if err != nil { return nil, err } @@ -4044,7 +3874,7 @@ func NewDownloadRecordingRequest(server string, params *DownloadRecordingParams) if params.Id != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "id", *params.Id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "id", runtime.ParamLocationQuery, *params.Id); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -8710,7 +8540,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZip(w http.ResponseWriter, r *http return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8744,7 +8574,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZstd(w http.ResponseWriter, r *htt return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8752,7 +8582,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZstd(w http.ResponseWriter, r *htt // ------------- Optional query parameter "compression_level" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "compression_level", r.URL.Query(), ¶ms.CompressionLevel, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "compression_level", r.URL.Query(), ¶ms.CompressionLevel) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "compression_level", Err: err}) return @@ -8786,7 +8616,7 @@ func (siw *ServerInterfaceWrapper) FileInfo(w http.ResponseWriter, r *http.Reque return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8820,7 +8650,7 @@ func (siw *ServerInterfaceWrapper) ListFiles(w http.ResponseWriter, r *http.Requ return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8868,7 +8698,7 @@ func (siw *ServerInterfaceWrapper) ReadFile(w http.ResponseWriter, r *http.Reque return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8963,7 +8793,7 @@ func (siw *ServerInterfaceWrapper) StopFsWatch(w http.ResponseWriter, r *http.Re // ------------- Path parameter "watch_id" ------------- var watchId string - err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "watch_id", Err: err}) return @@ -8988,7 +8818,7 @@ func (siw *ServerInterfaceWrapper) StreamFsEvents(w http.ResponseWriter, r *http // ------------- Path parameter "watch_id" ------------- var watchId string - err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "watch_id", Err: err}) return @@ -9022,7 +8852,7 @@ func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Requ return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -9030,7 +8860,7 @@ func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Requ // ------------- Optional query parameter "mode" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "mode", r.URL.Query(), ¶ms.Mode, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "mode", r.URL.Query(), ¶ms.Mode) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "mode", Err: err}) return @@ -9064,7 +8894,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "source", r.URL.Query(), ¶ms.Source, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "source", r.URL.Query(), ¶ms.Source) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "source", Err: err}) return @@ -9072,7 +8902,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req // ------------- Optional query parameter "follow" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "follow", r.URL.Query(), ¶ms.Follow, runtime.BindQueryParameterOptions{Type: "boolean", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "follow", r.URL.Query(), ¶ms.Follow) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "follow", Err: err}) return @@ -9080,7 +8910,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req // ------------- Optional query parameter "path" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -9088,7 +8918,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req // ------------- Optional query parameter "supervisor_process" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "supervisor_process", r.URL.Query(), ¶ms.SupervisorProcess, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "supervisor_process", r.URL.Query(), ¶ms.SupervisorProcess) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "supervisor_process", Err: err}) return @@ -9155,7 +8985,7 @@ func (siw *ServerInterfaceWrapper) ProcessKill(w http.ResponseWriter, r *http.Re // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9180,7 +9010,7 @@ func (siw *ServerInterfaceWrapper) ProcessResize(w http.ResponseWriter, r *http. // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9205,7 +9035,7 @@ func (siw *ServerInterfaceWrapper) ProcessStatus(w http.ResponseWriter, r *http. // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9230,7 +9060,7 @@ func (siw *ServerInterfaceWrapper) ProcessStdin(w http.ResponseWriter, r *http.R // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9255,7 +9085,7 @@ func (siw *ServerInterfaceWrapper) ProcessStdoutStream(w http.ResponseWriter, r // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9296,7 +9126,7 @@ func (siw *ServerInterfaceWrapper) DownloadRecording(w http.ResponseWriter, r *h // ------------- Optional query parameter "id" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "id", r.URL.Query(), ¶ms.Id, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "id", r.URL.Query(), ¶ms.Id) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) return @@ -12159,13 +11989,10 @@ func (sh *strictHandler) TakeScreenshot(w http.ResponseWriter, r *http.Request) var body TakeScreenshotJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - if !errors.Is(err, io.EOF) { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return - } - } else { - request.Body = &body + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return } + request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.TakeScreenshot(ctx, request.(TakeScreenshotRequestObject)) @@ -13045,13 +12872,10 @@ func (sh *strictHandler) DeleteRecording(w http.ResponseWriter, r *http.Request) var body DeleteRecordingJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - if !errors.Is(err, io.EOF) { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return - } - } else { - request.Body = &body + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return } + request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.DeleteRecording(ctx, request.(DeleteRecordingRequestObject)) @@ -13129,13 +12953,10 @@ func (sh *strictHandler) StartRecording(w http.ResponseWriter, r *http.Request) var body StartRecordingJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - if !errors.Is(err, io.EOF) { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return - } - } else { - request.Body = &body + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return } + request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.StartRecording(ctx, request.(StartRecordingRequestObject)) @@ -13163,13 +12984,10 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { var body StopRecordingJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - if !errors.Is(err, io.EOF) { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return - } - } else { - request.Body = &body + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return } + request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.StopRecording(ctx, request.(StopRecordingRequestObject)) @@ -13194,154 +13012,158 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x9e3MbN/LgV0HNbZWtW778yl68dX8ospzoYscqy77sJvRxwZkmiZ9mgAmAoUS7vJ/9", - "Cg3MG8MhKcm28tuqVEyReDTQDzQa/fgUhCJJBQeuVfD8UyBBpYIrwD9+oNFb+CMDpU+lFNJ8FQqugWvz", - "kaZpzEKqmeDj/1KCm+9UuIKEmk9/kbAIngf/Y1yOP7a/qrEd7fPnz4MgAhVKlppBgudmQuJmDD4PghPB", - "FzELv9Ts+XRm6jOuQXIaf6Gp8+nIBcg1SOIaDoJfhH4pMh59ITh+EZrgfIH5zTW3pKDD1YlI0kyDPA5N", - "8xxRBpIoYuYrGp9LkYLUzBDQgsYKmjMck7kZiogFCd1whOJ4imhB4BrCTANRZnCuGY3jzSgYBGll3E+B", - "62A+1kd/IyOQEJGYKW2maI88Iqf4gQlOlBapIoITvQKyYFJpAmZnzIRMQ6L69rG+IQZfCeNntuejQaA3", - "KQTPAyol3eCGSvgjYxKi4PnvxRo+FO3E/L/AUt9JzMLL1yJTsOsm1/dnnmlt6aG+PTgksb+aPWGG7Gio", - "yRXTq2AQAM8SA1sMCx0MAsmWK/NvwqIohmAQzGl4GQyChZBXVEYV0JWWjC8N6KEBfWa/bk7/bpMCIt60", - "cbipzBqJK/NnlgZuGO8EKxFHs0vYKN/yIrZgIIn52azPtCVRZroiju2oFeS2Rq+jbBDwLJlhLzfdgmax", - "RuQ2GCdL5iDN4jRLACeXkALVtXnd6Gbbl4D8fd1exT9IKISMGKcad6sYgKRCMbdn7ZE27ZH+echIDTK9", - "DszQHUSazgWV0UlFJO1OoxqudRvkk0xK4NqAaQcnph3JpV6LHhrQ4qBeYOucuq/MUowvY2hKrKrAooqk", - "VFqhY0XciLxbAfmXAeVfZMEgjoiCGEKtyNWKhaspL0dJQS6ETAaE8siiSUh7FEeGdm1vswmUGWm2ghyC", - "lEqagAapRlN+ek1DHW+I4MXvtmdi4MmZwABEkkxpMgeSSrFmEUSjKW9JWcvKiZEZvYKwJbDM0SLpcrfu", - "LyRdNnsnYg279X4t1tDsnUpQyoiJvs7npuHPsKn0VaEUcdzX8QJbVbuBnoWZVPac3toV9Ak2rPaOAdLe", - "jqZRedh0SNkcx8X5V6GwUUXeVvFb22878gyZqbqVxdbUcFtbeb4Qn+QuB+1Zpjkn3sG1LranyeVmZC+X", - "S6AaXjAJoRZyc9jhmYjIs6tvUtudRPnoxDQkD0WoaUzsKgcERssR+duzZ0cj8sIeFngW/O3ZM9RiqDZ6", - "XvA8+H+/T4Z/+/DpyeDp578Enr1KqV61gTieKxEbaVMCYRqaGUJcemOS8eh/9opMnMm3mS8gBg3nVK8O", - "28eeJeSARzjN7QP+FkI8+5aHQc+iNuxnkVFJUcNwp6nMJ6mshBzH6YryLAHJQiIkWW3SFfAm/unw4/Hw", - "t8nw++GHv/7Fu9j2wphKY7ox9xS23HM9K0BlrvPAjezYxLYjjJOUXUOsvLqGhIUEtZpJqqF/SNeamNZm", - "4J8+kocJ3Zjjh2dxTNiCcKFJBBpCTecxHHknvWKRj6Cas2GzrfB7t7Z5At2Nwm3EZoeyXSjZVuv2CdAI", - "Yrqp6aGTpqrywjQxq09YHDMFoeCRInPQVwA8B8Qo2qhpKE2ldtRr5D+hsXBaguGuEYLFWWIAnfhwEmUS", - "75+zxKOOv6NyCZpoYQRk3rIF20JInNCwlgS7QwaWxCD1agWcqEQIvfrfWmYwIm8SprEPzbRIqGah0bjN", - "GuZUQYS3OZwQ5UsMfOnWQa/tOh5NJpNJZV3PvAu7yS3DLGGvS4ZfUjbvsr9fD8jmQ1WlTymTqsCdXkmR", - "LVdGuYwtEEvGlyPy2qh6TnckVJMYqNLkMUkF41rV7rpNkCsbktBrd7F9XL3lPm6vZuuPFpc1GjZ4bZLx", - "ewVklSWUD2N2CeQH+Gg2PMzkGkpqRgxf0Y1dCGFcaaCR2aqYcaDSXm9TESPhjcivhphwNqI0pGqWgpwp", - "WCKlWXaAdIZMNksUoRIIW3IhIRqVUmQuRAwU1a9a89qSnu3JlxIMjGuwcLUweGahaHNDL3+21lm/xU66", - "r7EFSEhbFq4UJMn3i/FSTHQDSF5b8MijGqyPeq+dnYd7YQhrKG2gFF2Ch90aA+cNvWPbu9x5TDdXKIUP", - "M3G5XtXbYTkkCY162bpqeXVOowdf4N/j/0PX1H7EAWoGrXd4X4yArKgiNAxBoVB4kNIlPBiQB3h5vtYP", - "7O3ywVyKKwXyAVlTycxx666OSRrDczIN6BVlmpjOo6XQ4uGDldapej4eg20zCkXy4OjvRILOJCeV5prp", - "GB4e/X0aTLlPqdUsAZHpmYKwRofftejwtZXWbo14hWEJKg+OdQr1mjBFvpvUJPyTmnzvpzXc/B3pQSHA", - "e5KD6WR4qkEF5epa9AA5ldeHQuInjoSN3lTuz4KyGCLfrssC6PY9cU3jDBwmISLzjTM+mIsNWxDKN0dW", - "jEQgPfBcaMojKiNrNiULKRJ7llcW1oJH6UhkestgItNppncdLUOCbw/36wr0CmS5IMcvEXFdFlkcbzyC", - "vUEd+QQ+AnnJYjjjC9GWR0zNIia3Q4XnF1OEltc5/0GTiGhm6L893CtzlCeokNiHAOSTkbXRJlQHz4OI", - "ahhib8/u+e+6Zln2djtnWpGH5lI7INMgklfXcmj+mwbmYjMNhvJqKIfmv2lwNPLNwKkP7h+oAmJ+yi9S", - "CzOlkN6d2PlWnOusbSJhH2E232jw0MkF+4iCBX8ekQkqlzkYDNSo30aKa3TQ1SYb5HRQwaHb9C5yutgo", - "Dcnpujirm4hR2ICEK8qXQMA0bD+Q7EJ+dLGA0PDDznR4KC6LqQ5F6n5U4jeL4ZaiYaxqAzt5e3r87jQY", - "BL++PcN/X5y+OsUPb09/OX596rmH+YxRg26F5RVTGvHmWaPRis3a2jvGuGVgw9LAdU6IO71KFVLJc9V4", - "JZYdtHVMYrHEuTal6K08MbaJrKJzNaSSWBaHlNE8Rl3KgNI0ST0nkznrzfQlRFdUkVSKKAstFe0i3jo0", - "v+rUPoThnf3cPZC8de/hbQm/68tNbhc9/MWma4SdX2paBvL9jBu3eMlHi/ENr/cRU5ryEGo637O7vtQb", - "mPe61N/8pusEc3mtNR8p141d9MvqPvIsrQY5hREtDiLTXUfai1wPNztHoPSsz3wOShvg7QuaVRr6rM+D", - "QMmwb2AlMhnCzmM2Vc18gkFlFb4denNZlUt73EV+BI5W6Tc/k9zTpy3XxWUv1Z7xyBwLoHJletSvSItL", - "71rOqQ5XzrJ9GMa7TNsvuk3ahaB4/HSyv4H7Radhe0TOFkQkTGuIBiRTYB9rV2y5AqUJXVMWmyu37ZJL", - "RQlIPu6QdarJd5PBk8ng8bPBo8kHP4i4tTMWxdCPr4UzfElYGNmB7glGUbUiOGZrIGsGV0YJKd40xhJw", - "mUY1DDVbg1/SSEAz8ixcSZEwA/un7tmxKTlxTQldaJCV9edqrRYEuMokEKYJjWhqn9E4XBEDde32jzSB", - "e7kCGi2yeICzFd/EHeTZ+aLwovMloSCbJ48nu70rNJ+XDzt5e2z++ambH1uGpvAcQ0N/4yyukqhB92Rg", - "21IJRNM0tfrVdrPiloO0eCdN+k7US9gQfFt2zl72RN/9gPXP/8pZy83oapPMRYyT40QjckrDFTFTELUS", - "WRyRORBaaUtUlqZCamsLuY6EFiKe8ocKgPzj0SNcyyYhESwYRySqoxFxtjNFGA/jLAIyDd6iRWUamFvz", - "xYottP14omVsPx3H7quXz6bBaGot5taoypQ1+YcIII2VMFCGIpm7I0u5Z2Y73l91fhnHv3C2v76jcxx2", - "jw1tSGvcXa+8lsII/NNrCG/NPErN8hI0wW+4kSNcZMrr+CeXdUv77x/aXpx2JCqXmVGP1H5URdVMClG3", - "k/uXkTkLuN0PfNUjpitJJVuzGJbQIXaommUKPLfz5pBUWXIwrc1QPIvx9MhlfNv5zq7dc/nFjcaTR0ii", - "VhDHxZabsyDj3jtaeOUZ61chLw0Pl5fVh7R6WT9yIzrLm52Ecd8C+nUu4Otu8vrkeyJ1OPvU8m095Wsm", - "BceLR2H6NrAq0MVR7La+shsl5bfM1/tZrLsR2G2YtujsZcMbWaVplekKhBXraDPh1vtg6V3bdRkceW8Z", - "cM30zP8M4pZKTBM05fpHsEbq2fy7p34b1XdPh8BN94jYpmSeLRaWszqM1LsOJjLdPdjnbuz9zEoPsv3Q", - "d8GW5pBF6rU83KDeOsoUNq8JteDd6dvXwfZxq5Yy1/zns1evgkFw9su7YBD89P6830Dm5t5CxG9RFT30", - "NEE1lpLzd/8czml4CVH3NoQi9pDsL3BFNMiEmZWHIs4SrvqeKweBFFd9Y5kme7574qgDC+iWHbtI6VXN", - "AT+O3yyC57/3+Tq2ju7Pg6Zdi8axMFe7mdab/lPw2LUmlKQKskgMi9U/PH/3z6OmYLWaPR5EufM5vnub", - "E6njuPQj7czoX4ZSG4izF5rqIswdofVavgdKWzOZZodP0xYHH1p4PUCen1UMxnRuBBIlyoy2jR9Sn5fb", - "m4sCWWcv/KLW/T7zdbcRLEOqDN9DRFjpNOc5ZAs7bpaxyC+IqVHHZ1T77cRox7XYqJKZ67aHqbiT1TTV", - "mdoTG7lTmsLO9pTtlkppNktDz/pOlWYJNZeRk/P3JEN7egoyBK7psnoKcnTb6DlGT/Pjk7BFba9W1J6t", - "drv6dJRBkEDS9ZhWQixBIeZJAonRES30xTtbxwnuNbeclzjVtccbmXFu0GeXDZH/LOpGbMQODGJ6QTU1", - "kuxKMmsAbZCefcdmPM08b3MR1XQnxSKqzjLqtR4W437oXfON9EUDjvMZVGa49gpNCw28i0hKJyNsQFzz", - "UbCrScUtRQItH0r30Z0uTklKN7GghkxTCcpIKL4sMOgcEIQkMVtAuAlj99CqborN4mGtJBazCq8KCv53", - "uld1kFovmoYVvN6jO4mGQpDawZkiU+w4DbpY1sDvOQWsIdz+nL9k4RaEq4xfVgF2/iCFl8luTGzdu0H6", - "3S8WjDO12u3YKH24815dh0bv/dueh+2vVeGMXvm95km48yFXQus6HQhsQ3jg4VuF0ydELkIJwNVK6Lew", - "3CWMajc7/U/WPl+41C/dpXGLA3qH5fZXtNjuM9COr7h2rAdGfU2HMSwMt0gON3rX3WNM79NZvguDfGP7", - "UHaIBVoWiO6JhaoThpdl6xFT+77qxZrOrrcbwn8Skn0UHONxcC5CE5FxPSL2Od9cNPB7RdALb0A4LGnt", - "e4MHv6SzEPS43/9fA3G4w/yRuOKe6bPUP/lNXq6LmK3djaB9XEG1DWGsBJbVp9qfKfYecufn5Fa03Z5S", - "i0UR8B7/QvvsXb4puE69b6KuXQfYL1kM5+bWqRQTXB0G/1KKLPUbKvAn57olyY+1296+PoKeMLjvnj49", - "2i/qTVxxn13cwIo/oSU8h/d9B7y7+JNdrYTCu1S+t/b5y7604BNkdGhE2hb/vmr45n4q6znNFFS9fYXE", - "+z2Ehvejwta6p7G2+nKIcZs+W23Vr7rmZDPpZcrq5N4NMSrMS/Ur1eGtBhkWEaB4fcJgbL9ntGFctoZ+", - "O1fB7W48UvSNNzv4PnR6cuAO3DBUcSFpAn5Phbelbps3MihepIZj1yAli0ARZZOOuB04quL88aTPaOY1", - "IeWPwB7jT0WBBeS9WwqYRKBzgj7jF5aAux9qSjiqDxW5w9r23dm6IQm9Rkde9hHO+OsfuiFAr0/l3I9f", - "/7AjRprxa4929ES40CK9KaEJGYIZp59fzpIEIkY1xBtMs4LPoyLTZClpCIssJmqVaaMFjci7FVMkQX8a", - "tDEwjg/CUmaphoisWQQCN8tvH94nUtdysAHoDsN0m+Hre2u6NwvyNHqgluISVK8fhz8Hh4EdjVqYRMCa", - "A1YCPRJs+ovD03D8KpmGInHIYRu0HeiaSS73lc8nPBRw04w5QwMGOQXPg59BcojJWUKXoMjx+VkwCNYg", - "lQVnMno0mqBKkwKnKQueB09Gk9ET5ymOCxnnHlPjRUyX+XEWes6z1yCXgN5P2NL6GsA1U2i2EhzUgGSp", - "uf2TxqAen6s1o0RlKcg1U0JGgymnPCIYxZVxzWLcuaL1C1i/EyJWZBrETGngjC+nAXpmx4wDYYqIOYor", - "o/guhMzDiVDCO+dAdEQxOLTCOUKNRoerfJaXuH6LClD6BxFt9sp11RBT+W42bPL5kuweakES3FYX3vL7", - "NBgOL5lQl9YxZziMmKLzGIbLNJsGH44O96WxAPnJqmynZQbWna7MwPZ4MvGo3gi/xXeEMX3F0hyym0FO", - "nwfBUzuS7xZfzDhuJnz7PAie7dKvni0NU4dlSULlJngevLd0WYAY04yHK4cEA7yDGbuV1JuKmIWlktfN", - "FeZKMMzT2JTTAMb+SmbUfDPUhpSnK+NOPsxp8fPIUNVgynvZhezPLVO+L7ucgMRw7XwXSEI5XVqvtEsr", - "eBhfSKq0zEKdSUfF5PRaAzci6AK0kQ1qMOWpFNebIcbzQlSMaNdRjJ+TIappJy/Ox7n/veBHeDeaxyK8", - "hGjK0YSd72UvZ5/naDycuf1Hg8/LdRfkj8jPubej+8ncJ9WUP3Q+dc6z9ESISwbK7eM0OML9wnhJd1lc", - "FSPYb0dTfgFA8mhZpGQoIRkthVjGUBD22F7iCo/g/Hu7pS7W1qbeUyw8zvTqzRrkT1qnp/h+HuV74AUY", - "dVPTWL1Pl5JGoIpe7lB9Ta9PBOdg09Cdgzw3dBI8f/J4EJyLNEvVcRyLK4heCvlexgrNFe1I4ODD59uS", - "azmt3FvR1iQ7s5ZuCZelsaDREHKWVUPKo2He1og9oTyKznvshjm+hCSJkSDFEOQjSwmV4YqtDYfDtcbs", - "fnoFCcm40X7HK5HA2IqQcTn1eJpNJk9Cwwr4CQZTrkATaWRcUp3Bym3GD1A0Csk55V9Q0bD7VQhGdcyj", - "t26Pt8mkJIs1S6nU44WQyTCimm7TOcqt7HZJLtsY5cOiH/cEnWDM/b2iYdSH90devhSxwSkaxLQgaUxD", - "cBHTObr2w3rj7nM8/I0OP06G349mww+fHg0eP3vmt9t9ZOnMXNDaIP5WEmSeg8TgixrIUuutVbJPAfVD", - "TE+Xu1MnlLMFKI1H9FH1vWvOuOHEPq2+AM+FsPpuJlsVuAp2D9PiHvneXAtqsKQA0cAj7SzXFMzBzFFN", - "o68t91oiqMBmhcgfUmUEkjqqCsFiiU4auivleJ7reH6pd5p7inMiGnlxWmlk0X7gMjYen5+RkMbxiBy7", - "X/Hktw8MRp2pJpp1iVdWIo4ckcJ1GGfKEK9RfwZECcIFEWgKRPcOUggbRULKrVNbDHQNmFSjL9Nske8x", - "33jCisgq+xyS53HE9A6jKUdjifUJX2Qx6hDhynFVBNZHzdwLwyKqAt2PbMigme0SNjaxptuuKc9NMynd", - "mFE46CshL4kUGY+GWrKUGNWRhxucDTCEgkdszaKMxm4Yn+T15Ay+gRq47f1xS3biQ5URHLIjZ8TX5L2C", - "EbbkUa7SdIPNGjk9c2arI67M5nlH+PKkCz0QTTbBWp4MNWfrr4qhC5ZksXWJtVxXTXfst6e1cGTNVWMj", - "6rvR9BZodFIxbfl267bQVc/060ueXiTsdVPiOdXimxvvrlm0zQlc+FK1rHxd24m2we79rBsn74j0/RbQ", - "Q8kfrZ7Ofw6zgBZY+GYE1q/WIJvblHfAV5FD14+m4j3/jjDUzs67M3JuZf5KcLePz6yrwZopNmcx05vi", - "tvzNYPwnFrkwM3FVzWBRR3M9O7Rf68PoWdRa0KklF6g2jeWACPfMaDQ3mueNMNNKTfAZZWCm583Ulku2", - "zrMHWsU0BqoAdatqUqaevIs+jafIInpHpNnOk32g3DADfSPHJYJS5gaxaKKIhwbFLEFbgpkV6es7hcSP", - "oGt5XO7yePQnjPHzLkYF2JUWi7iNXfwRdM5qlSmcX1I+0y7KRz3tun9zi3wyd0Tm7YTuN9IO3S6YlX1d", - "Un+dp0mpYSc/FQtnnlLSqF0wVkt1v0WOulwU5TzoMIgykxeitPQksnby0qWtElA/5b4w+RF5ifLXACZh", - "Bdzem9vx+AOiAKbcAOOPqSdUl2b0JdOjhQSIQF1qkY6EXI6vzf9SKbQYXz96ZD+kMWV8bAeLYDFaWXnu", - "PClWggupqg/mwxjWUK7X3Kidn0zotgI9opQzoVksiMj74uGSPNwRO7RKFBzIDYhQpJZvSVuwZ3zVloR0", - "uQPhq8LruFtUvaOXUHon35XG2HKy/uxwtPXEYQldwji1QQHlTP3WzdbBUgJAcNCvitATmuKLJCUlgnIv", - "nB50urIbfiFm3cfJ2rlYxxujvY2F4e3c7dt8pys6XkWS1rXFmp2vlqnEqYE1/22XC5qTWCzRu1uz8FKR", - "h1xoF1tgTZwVCiJzWNE1MyRNN2RN5ebvRGdopXOp73MGHk05JqaeC72qLMU+N+bu5Oh87myX7ql7YKW5", - "FW84sxXwSc38Qx4WY6AqXE5wZP0+0IqE1kaA2EU5OVH4LyfYnQFjOHQVjX4hwyGq12RC7AuCVcjtG8K/", - "fBLyIvfiviP2q1ZiOVA6OvL6RmxIFphSV7Doodpoxntoc3kqzA7h6BzV7ggv7TIuNzBymJV8Q6cWljJD", - "o0Y3FlxFipoHi8dVwqWbuivlwZNe7QsbNOplSzzH13tnwchLeITYMs99dQM0P51839+vXmXyFv0COpZj", - "SGOhxrZgz6zIooNkkvms8fWiRndlkveXTjr0dbP0wLfr/IZY166UUPSnLLc/x4ut4rMDXmyZobvGS7sK", - "08E2nwIldonRzTjraX+/evHSWzEWIeTVTNVNvOVuCFtQ9tK6Anzb2ML4qj8BohAfBY7EFY8FjQx3zT4y", - "jCNYgvbFrehMckUo+e3s3AZKVLxHbGIxRJfKbxaVWKhqcvAG/t38L5j8jaXo7ZKXT8TkOTtXW8tdWowG", - "nS8K88yZfn9kgOLAOu3kUWF1GhhUPYn6osw+7HU4u3290YXS7Hq+xiKAAgmrusH3kS4dsqoihNCc0NyS", - "O+hV6WgHgtVUjj4qTR5qKiuuT0lueEHffTPW0Va6nvIthE1+UzoiYrEAqYhiS471H7iON2RBlQZZTIjp", - "gHg05RFUvzKfqQRMHPaRpe5CTMMVgzUm2wbdHAXZyP/qUeEqs0f3ha0Gn9qpI4vlonVwRH5iyxVI+1eR", - "gZ6ohMYxFOhVZJ5pouklkFjwJcjRlA8tJpR+Tv5tsG2HII8GxEVyGcRCRB7++8lkMnw2mZDXP4zVkeno", - "An/qHZ8MyJzGlIdGlTI9x4gB8vDfj55V+lrE1bv+bZDjM+/ybDL8X7VOLTAfDfDbosfjyfBp0aMDIxVq", - "meEwQRUdZeK5/FOZAsRtVTCo/GZBxg/Kl9BkX6nouPdGYvGd4+3/ZqJR15ddiEcjv2Z5XJQTi3XRUJSi", - "2FUm9Fb7+BZO2P10wrIcR5ugUMur1Pq4h2TzI+hatZI8+VwLewXZxExp1NNVJ92URVMOO0zuJ6WUq/aQ", - "Snl9i23c3z2kFfSER8xbJ902bWCZja7rW14Y4g6fnW/j6obPvKW54x7iCVeApQAwtmAbM0ugUXHp9vLy", - "W6CRu3Lvxso4Wa4SmvG/FW4WoQY9LFOe3UiXQNHv9ZG8Z8SCHpnFVcZ0LIhDgRX0s0qmlU7ubie8uTsH", - "v47MOgdHrlUSyTh3vHuIyAvQnkpkFdSNMQmPWrG0wLANXel+tMUYwjzCBSO1bFyGkMRGWMXgDgTnBiMh", - "EU4GWD/RUUdEV64e3FoIV6GRdMRgHVJYqJKRwCm0u5UaygXqvpFOLsppe/Wg7bHquAu3FuWEWCoCnO67", - "qPMEPi2cvlZlh9y0uTWAk6LhBfnN5tu3sZpMq9K22XIN8xWu8jGHtW7eGmvsS/pRNf9SJQq1uDhrsRsf", - "VAMLbxD1t40fDiTs31haknUFgX8aIqfVYOIGibbo3RlXegh+X9NoF19MeT9j9JtIaxbRKW+YRLtDiZ2N", - "89aYK7eqeAs0N0wvxRHSywyDr8e05lM6K+luezKjMht0DFZFwIOz7G4zNkmW5kktHWwYKIxFEw05DYfY", - "Zlj2O+qrY9WQFzke7kRcHLs9/JOLjCa5doiNq2awb+MmUEkLeFd3AE/mwd1xe2BiIly2t2jCe87+yMCX", - "Lq/kyiu3Hb0ZyNp3TVwmue38GV+J2OxiqkZqFwTNlxVNDHdr/Cnf8s8utRrYAMAmvYm0JLeGkQIND87S", - "4OwOBR632R76TQ2enPE5okSa3n9EXWDeP7MijKb3GI+aSBpb/9NOU5LN+f9SndpmXxBXTbOQhmttofXa", - "g/reA6pV3n3+3BenldT55V3Y+ediym8a4ao/Bf8YXlycDl1o7vCdt/j5a4gYdQn9FsQMj7n4nbvvw6YQ", - "O6q93OWvdC1R53mU+3wfyRQ3urXLLpzQit2CYs1lfruTEQa87mLwfFFRvmjL+PkF372LdK2LIqlzZz7n", - "vPIqqmXfPX3aBSYmQe4Aa2sWaMt8u5z4NzTHHmjNKMKt7/sximYpc3Lm/pClq1Yslmpcbqz/iU4sXQ2W", - "DjncIAhbGnsr5eaCxpF4mTvKWxPEP81CxLG48nse1AphVFI1N9EseLwpM+KxRV7WmyniQNvCmN2nyj7z", - "VNbun61sMHO1ZIKvdqK9EssdjzJDWN/06eU7GQzQmEDQTG0ZJI3p5gprSIxdipgdUhfJOdOSyg05L3q7", - "elzccB+WEi9TvCNqrjWhS8q4sjfxuRRXCiRxha+mXHASi5DGK6H08+8fP348Iu/QiSwCLOtFw7zo3oOU", - "LuHBgDxw4z6wiaUeuCEflCVRXQSULAo+6XzEEjhMQ6UziQXeeC2Dkc9w4ragXPeJPR3u4mbXmusrRT14", - "4MCyW7648HJzv8VUQ+USMKTnAiG3FOEhTscgViYhd3Rf9CsFKe8sdrZd8vLL0kG7UK+HAspMYdK1+SZS", - "THmrctcRjDUmezGMdS3vFsW1kqhfB8fV6p2+o9CW4/zGcEu3IPdTWejz8/iS1aNzvYj+mWGYZ/+9vFJC", - "dJtK2FMfdPfLwkEIrdZn/qayAL35+V76FxhRUhSYztXWboqTWNu5l+ZsCeg/D9XVy2H/h+5u7qDUWSJ8", - "C/Gpou6v9/pbrw78pWnvjs8xuyjfEeZ+uZdeypUCvXZ53aiP2A46Dbb600idWjnkr6Q/VaoTe4jvh2q1", - "4HtrcStPPls+eTsdikz3GeLKzROZ3mqR+0ry6AaWJU+t514bU6OKs9Fxm2Wc//OAcgcPKBWqFpluGMyK", - "amvj8hHWL11t5HBZiPguA7Vb9dC68zZ11dX7aiHaXym3RRHYnUpYM7wz5rXVqqXaWlh3wWWdUiyPPqsi", - "fuvrWfFoVVR2K70nRgRTKonEHBX1TElZngfPvQoU3bseslDo+Z+x+mrD9YtG3LBxkj69cThBpdKjfXqs", - "Cbji1+FLV+N8eLy11rhYlKXg2wXSR+THjErKNVh/uTmQty9Pnjx58v1o+wtIDZQL649yECTOl+VQQAwo", - "jyePtzE2M5KMxTEWEJdiKUGpAUkxVyzRcmNtn5gaX9a3+y1ouRkeL7Svlu1FtlzaWFFMWYvVVSp1J8vK", - "JnJjmaBcxLayk/fx3CgCTm2aK4W8COiiuYNEiZk9PTrjB986xlY3zf1axANsO1Dy2WykZ8vJvsWveVEY", - "WUB5awF2NI6rw9a3rVVdyON6d9eHr7/orffsfbSNRZ0QuIcZonAHigyJpVwbkTc83mCAQSnrUpDk7AWW", - "F8G8gUumNFZAwXRwRoKM2lgW6TYkV0rB3hmOPeVm91evnCvc103Gp0VaP35wIf8/AAD//yXWne87vwAA", + "H4sIAAAAAAAC/+y9+XMbN/Yg/q+g+jtVlr7DSz4yG0/tD4osJ9rEscpyNjMJvRyw+5HER91AD4CmRLs8", + "f/sWHoA+2GhekmIr+6lKxRSJ4wHvwMPDOz5FschywYFrFb38FElQueAK8I/vaPIO/l2A0udSCmm+igXX", + "wLX5SPM8ZTHVTPDhfynBzXcqXkBGzae/SJhFL6P/b1iNP7S/qqEd7fPnz70oARVLlptBopdmQuJmjD73", + "ojPBZymL/6jZ/XRm6guuQXKa/kFT++nIFcglSOIa9qKfhX4tCp78QXD8LDTB+SLzm2tuSUHHizOR5YUG", + "eRqb5h5RBpIkYeYrml5KkYPUzBDQjKYK1mc4JVMzFBEzErvhCMXxFNGCwC3EhQaizOBcM5qmq0HUi/La", + "uJ8i18F8bI7+ViYgISEpU9pM0R55QM7xAxOcKC1yRQQnegFkxqTSBMzOmAmZhkxt28fmhhh8ZYxf2J4n", + "vUivcoheRlRKusINlfDvgklIope/l2v4ULYT0/8CS31nKYuv34hCwa6b3NyfaaG1pYfm9uCQxP5q9oQZ", + "sqOxJjdML6JeBLzIDGwpzHTUiySbL8y/GUuSFKJeNKXxddSLZkLeUJnUQFdaMj43oMcG9In9en3696sc", + "EPGmjcNNbdZE3Jg/izxywwQnWIg0mVzDSoWWl7AZA0nMz2Z9pi1JCtMVcWxHrSG3NXoTZb2IF9kEe7np", + "ZrRINSJ3jXGKbArSLE6zDHByCTlQ3ZjXjW62fQ7I37ftVfyDxELIhHGqcbfKAUguFHN71h5p1R7pn4eM", + "tEamt5EZuoNI86mgMjmriaTdaVTDrW6DfFZICVwbMO3gxLQjXuq16GENWhw0CGyTU/eVWYrxeQrrEqsu", + "sKgiOZVW6FgRNyDvF0D+ZUD5F5kxSBOiIIVYK3KzYPFizKtRcpAzIbMeoTyxaBLSHsWJoV3b22wCZUaa", + "LcBDkFNJM9Ag1WDMz29prNMVEbz83fbMDDyeCQxAJCuUJlMguRRLlkAyGPOWlLWsnBmZsVUQtgSWOVok", + "ne/W/ZWk8/XemVjCbr3fiCWs984lKGXExLbOl6bhj7Cq9VWxFGm6reMVtqp3Az2JC6nsOb2xK+gzbFjv", + "nQLkWzuaRtVh0yFlPY7L869GYYOavK3jt7HfduQJMlN9K8utaeC2sXK/kJDkrgbdskxzTryHW11uzzqX", + "m5GDXC6BanjFJMRayNVhh2cmksCuvs1td5L40YlpSI5ErGlK7Cp7BAbzAfnbixfHA/LKHhZ4FvztxQvU", + "Yqg2el70Mvo/v4/6f/vw6Vnv+ee/RIG9yqletIE4nSqRGmlTAWEamhliXPraJMPB/79VZOJMoc18BSlo", + "uKR6cdg+blmCBzzBae4f8HcQ49k3Pwx6lrRhv0iMSooahjtNpZ+kthJymuYLyosMJIuJkGSxyhfA1/FP", + "+x9P+7+N+t/2P/z1L8HFthfGVJ7SlbmnsPme61kAKnOdB25ixya2HWGc5OwWUhXUNSTMJKjFRFIN24d0", + "rYlpbQb+4SM5yujKHD+8SFPCZoQLTRLQEGs6TeE4OOkNS0IEtT4bNtsIf3Br10+gh1G4jdjsULZLJdtq", + "3SEBmkBKVw09dLSuqrwyTczqM5amTEEseKLIFPQNAPeAGEUbNQ2lqdSOeo38JzQVTksw3DVAsDjLDKCj", + "EE6SQuL9c5IF1PH3VM5BEy2MgPQtW7DNhMQJDWtJsDtkYMkMUm8WwInKhNCL/6llAQPyNmMa+9BCi4xq", + "FhuN26xhShUkeJvDCVG+pMDnbh301q7jZDQajWrrehFc2F1uGWYJe10ywpJy/S77+22PrD7UVfqcMqlK", + "3OmFFMV8YZTL1AIxZ3w+IG+Mqud0R0I1SYEqTZ6SXDCuVeOuuw5ybUMyeusutk/rt9yn7dVs/NHiskHD", + "Bq/rZPyLArIoMsr7KbsG8h18NBseF3IJFTUjhm/oyi6EMK400MRsVco4UGmvt7lIkfAG5FdDTDgbURpy", + "NclBThTMkdIsO0A+QSabZIpQCYTNuZCQDCopMhUiBYrqV6N5Y0kv9uRLCQbGJVi4Whi8sFC0uWErf7bW", + "2bzFjrqvsSVISFsWrhwk8fvFeCUmugEkbyx45KQB68nWa2fn4V4awtaUNlCKziHAbmsD+4bBse1d7jKl", + "qxuUwoeZuFyv+u2wGpLERr1sXbWCOqfRg6/w7+H/oktqP+IADYPWe7wvJkAWVBEax6BQKDzJ6Rye9MgT", + "vDzf6if2dvlkKsWNAvmELKlk5rh1V8csT+ElGUf0hjJNTOfBXGhx9GShda5eDodg2wxikT05/juRoAvJ", + "Sa25ZjqFo+O/j6MxDym1mmUgCj1REDfo8JsWHb6x0tqtEa8wLEPlwbFOqV4Tpsg3o4aEf9aQ79tpDTd/", + "R3pQCPCe5GA6GZ5ao4JqdS16AE/lzaGQ+IkjYaM3VfszoyyFJLTrsgS6fU9c0rQAh0lIyHTljA/mYsNm", + "hPLVsRUjCcgAPFea8oTKxJpNyUyKzJ7ltYW14FE6EYXeMJgodF7oXUcrkODbw/26AL0AWS3I8UtCXJdZ", + "kaargGBfow4/QYhAXrMULvhMtOURU5OEyc1Q4fnFFKHVdS580GQimRj6bw/3kznKM1RI7EMA8snA2mgz", + "qqOXUUI19LF3YPfCd12zLHu7nTKtyJG51PbIOErkza3sm//GkbnYjKO+vOnLvvlvHB0PQjNwGoL7O6qA", + "mJ/8RWpmphQyuBM734q9ztomEvYRJtOVhgCdXLGPKFjw5wEZoXLpwWCgBtttpLhGB11jsp6ngxoO3aZ3", + "kdPVSmnIzpflWb2OGIUNSLygfA4ETMP2A8ku5EdnM4gNP+xMh4fispzqUKTuRyVhsxhuKRrG6jaws3fn", + "p+/Po17067sL/PfV+U/n+OHd+c+nb84D97CQMarXrbD8xJRGvAXWaLRis7b2jjFuGdiwNHDtCXGnV6lS", + "KgWuGj+JeQdtnZJUzHGuVSV6a0+MbSKr6VxrUknMy0PKaB6DLmVAaZrlgZPJnPVm+gqiG6pILkVSxJaK", + "dhFvHZpffeoQwvDOfukeSN659/C2hN/15cbbRQ9/sekaYeeXmpaBfD/jxj1e8tFifMfrfcKUpjyGhs73", + "4qEv9QbmvS71d7/pOsFcXWvNR8r12i6GZfU28qysBp7CiBYHkemuI+1FroebnRNQerLNfA5KG+DtC5pV", + "GrZZn3uRkvG2gZUoZAw7j7muavoJerVVhHbo7XVdLu1xF/keOFql3/5IvKdPW66L661Ue8ETcyyA8sr0", + "YLsiLa6Da7mkOl44y/ZhGO8ybb/qNmmXguLp89H+Bu5XnYbtAbmYEZExrSHpkUKBfaxdsPkClCZ0SVlq", + "rty2i5eKEpB83CHrVJNvRr1no97TF72T0YcwiLi1E5aksB1fM2f4kjAzsgPdE4yiakVwypZAlgxujBJS", + "vmkMJeAyjWoYa7aEsKSRgGbkSbyQImMG9k/ds2NTcuaaEjrTIGvr92qtFgS4KiQQpglNaG6f0TjcEAN1", + "4/aPNIF7uQCazIq0h7OV36Qd5Nn5ovCq8yWhJJtnT0e7vSusPy8fdvJusfn7U9cfW4am8BxDQ//aWVwn", + "UYPuUc+2pRKIpnlu9avNZsUNB2n5TpptO1GvYUXwbdk5e9kTffcDNjz/T85abkZXq2wqUpwcJxqQcxov", + "iJmCqIUo0oRMgdBaW6KKPBdSW1vIbSK0EOmYHykA8o+TE1zLKiMJzBhHJKrjAXG2M0UYj9MiATKO3qFF", + "ZRyZW/PVgs20/XimZWo/nabuq9cvxtFgbC3m1qjKlDX5xwggTZUwUMYim7ojS7lnZjveX7W/jONfONtf", + "39MpDrvHhq5Ja9zdoLyWwgj881uI7808Ss3yMjTBr7iRI1wUKuj4J+dNS/vvH9penHYkKueFUY/UflRF", + "1UQK0bSTh5dROAu43Q981SOmK8klW7IU5tAhdqiaFAoCt/P1Iamy5GBam6F4keLp4WV82/nOrj1w+cWN", + "xpNHSKIWkKbllpuzoODBO1p8ExjrVyGvDQ9Xl9UjWr+sH7sRneXNTsJ4aAHbdS7gy27y+hR6InU4+9Ty", + "bT3nSyYFx4tHafo2sCrQ5VHstr62GxXlt8zX+1msuxHYbZi26NzKhneyStM605UIK9fRZsKN98HKu7br", + "MjgI3jLglulJ+BnELZWYJmjKDY9gjdST6TfPwzaqb573gZvuCbFNybSYzSxndRipdx1MFLp7sM/d2PuR", + "VR5k+6Hvis3NIYvUa3l4jXqbKFPYvCHUovfn795Em8etW8pc8x8vfvop6kUXP7+PetEPv1xuN5C5uTcQ", + "8TtURQ89TVCNpeTy/T/7UxpfQ9K9DbFIAyT7M9wQDTJjZuWxSIuMq23Plb1IipttY5kme7574qg9C+iG", + "HbvK6U3DAT9N386il79v83VsHd2fe+t2LZqmwlztJlqvtp+Cp641oSRXUCSiX67+6PL9P4/XBavV7PEg", + "8s7n+O5tTqSO4zKMtAujfxlKXUOcvdDUF2HuCK3X8j1Q2prJNDt8mrY4+NDC6wHy/KJmMKZTI5AoUWa0", + "TfyQh7zc3l6VyLp4FRa17vdJqLuNYOlTZfgeEsIqp7nAIVvacYuCJWFBTI06PqE6bCdGO67FRp3MXLc9", + "TMWdrKapLtSe2PBOaQo721O2WyrlxSSPA+s7V5pl1FxGzi5/IQXa03OQMXBN5/VTkKPbxpZj9Nwfn4TN", + "Gnu1oPZstdu1TUfpRRlkXY9pFcQSFGKeZJAZHdFCX76zdZzgQXPLZYVT3Xi8kQXnBn122ZCEz6JuxCbs", + "wCCmV1RTI8luJLMG0DXSs+/YjOdF4G0uoZrupFgk9VkGW62H5bgftq75TvqiAcf5DCozXHuFpoUG3kUk", + "lZMRNiCu+SDa1aTiliKBVg+l++hOV+ckp6tUUEOmuQRlJBSflxh0DghCkpTNIF7FqXtoVXfFZvmwVhGL", + "WUVQBYXwO91PTZBaL5qGFYLeozuJhlKQ2sGZImPsOI66WNbAHzgFrCHc/uxfsnAL4kXBr+sAO3+Q0stk", + "Nya27t0gw+4XM8aZWux2bFQ+3L5X16Gx9f5tz8P216p0Rq/93vAk3PmQq6B1nQ4Edk144OFbhzMkRK5i", + "CcDVQuh3MN8ljGo3O/0P1j5futTP3aVxgwN6h+X2V7TY7jPQjq+4dqwnRn3N+ynMDLdIDnd6191jzODT", + "md+Fnt/YbSg7xAItS0RviYVqEkaQZZsRU/u+6qWaTm43G8J/EJJ9FBzjcXAuQjNRcD0g9jnfXDTwe0XQ", + "C69HOMxp43uDh7CksxBscb//3wbieIf5E3HDA9MXeXjyu7xclzFbuxtBt3EF1TaEsRZY1pxqf6bYe8id", + "n5Nb0XZ7Si2WJMC3+BfaZ+/qTcF12vom6tp1gP2apXBpbp1KMcHVYfDPpSjysKECf3KuW5J837jt7esj", + "GAiD++b58+P9ot7EDQ/ZxQ2s+BNawj28v3TAu4s/2c1CKLxL+b21z1/2pQWfIJNDI9I2+PfVwzf3U1kv", + "aaGg7u0rJN7vITa8n5S21j2NtfWXQ4zbDNlq637VDSeb0VamrE8e3BCjwrxWv1Id32uQYRkBitcnDMYO", + "e0YbxmVL2G7nKrndjUfKvulqB9+HTk8O3IE7hirOJM0g7KnwrtJtfSOD4lluOHYJUrIEFFE26YjbgeM6", + "zp+OthnNgiYk/wgcMP7UFFhA3rungEkE2hP0Bb+yBNz9UFPBUX+o8A5rm3dn44Zk9BYdedlHuOBvvuuG", + "AL0+lXM/fvPdjhhZj1872dET4UqL/K6EJmQMZpzt/HKRZZAwqiFdYZoVfB4VhSZzSWOYFSlRi0IbLWhA", + "3i+YIhn606CNgXF8EJayyDUkZMkSELhZYfvwPpG6loMNQA8Yprsevr63pnu3IE+jB2oprkEFA7OCdupw", + "8FjbgzGI6zUXRv+2WgHiXThrroyUzNitObjMUgZjjrF59mEg6dmcHwbOVQ6JWeiNkEnfsEpi7Qfuud1P", + "NeaMa0n7ppkdUmGsE6e6kEYMcQ3S/pqbk1TZoClsaVNkjDlTPtovuFtjHiS+cBYTg300C2IaBmtQWQj0", + "6bAJRDrc1sXEKB8xbEb/pRRTOmUp0ytyNBqM+qPByejY7qoZBOP04gWVNNYge06vZuiKxsccU4ug4z/u", + "4ZTG1yqnMQww9RaTRvzYtVt9yDrmXqGTLxmNuRYkYcps+4C8X+V43UH0E4m+W0yR0WD0tD8ajF7YbStl", + "12hwEiQ6b7/eNZvLr5JpKPPPHMZnmzHXsOz6kAs/4aFpaEwz5uxVGCsXvYx+BMkhJRcZnYMip5cXUS9a", + "glQWnNHgZDBCzTgHTnMWvYyeDUaDZy7gABcy9I53w1lK514rigNq0RuQc0AnOmxp8Q+3TKH1U3BQPVLk", + "iVES1gYNuO4tGSWqyEEumRIy6Y254TgMBiy4ZinuXNn6FSzfC5EqMo5SpjRwxufjCB38U8aRaMQUTz1z", + "f5oJ6aPSUFFwPqZITAaH9oxPUDHW8cLP8hrXb1EBSn8nktVeKdPWTju/m2tPO35Jdg+1IBluq4uS+n0c", + "9fvXTKhr69/V7ztm6c/zYhx9OD7cJcsCFCarqp1hV+uVWSXyezoaBW5wCL/Fd4LislyaQ/Z6rNznXvTc", + "jhQyBpUzDtfzBn7uRS926ddMuocZ6Ioso3JlDhlLlyWIKS14vHBIMMA7mLFbRb25SFlc3RW6ucLcLPs+", + "G1I1DWAIuWTmtmiGWpFKSWPcyYcpLX8eGKrqjflWdiH7c8uY78suZyAx6t/vAskop3Pr3HhtBQ/jM0mV", + "lkWsC+momJzfauBGBF2BNrJB9cY8l+J21cewcEjKEe06yvE9GaK2f/bqcujDOAQ/xiv2NBXxNSRjji8h", + "fi+3cvalR+PhzB0+GkLO0rsgf0B+9E6z7idOM1BjfuRcM52D8pkQ1wyU28dxdIz7hWG3zuawKEew3w7G", + "/AqA+KBrpGSoIBnMhZinUBL20NoCSsdy/73dUheybTM4KhafFnrxdgnyB63zc6tt+T0IAoxXHNNY/ZLP", + "JU1Alb3cofqG3p4JzsFmM7wEeWnoJHr57GkvuhR5kavTNBU3kLwW8heZKrR6tQPKow+f70uueVp5tKJt", + "nezMWrolXJGngiZ98Cyr+pQnfd/WiD2hAorOL9gNU8UJSTIjQcohyEeWEyrjBVsaDodbjUki9QIyUnBz", + "iRouRAZDK0KG1dTDcTEaPYsNK+An6I25Ak2kkXFZfQYrtxk/QNEoJeeY/4GKht2vUjCqU568c3u8SSZl", + "RapZTqUezoTM+gnVdJPOUW1lt2d71cYoHxb9uCfoS0V1I0ytOXw4gPe1SA1O0a6qBclTGoMLvPfo2g/r", + "a1fo0/5vtP9x1P92MOl/+HTSe/riRdj8+5HlE3PPb4P4W0WQPpWNwRc1kOXW6a9inxLqI8xy6L3yM8rZ", + "DJTGI/q4/mw6Zdxw4jatvgTPRUKHbiYbFbgadg/T4k5CT/clNVhSMPfntrSzXFMyBzNHNU2+tNxriaAS", + "mzUiP6LKCCR1XBeC5RKdNHT36uHU63hhqXfuAw44EWvplVrZiNEM5RJ/nl5ekJim6YCcul/x5LfvVEad", + "qecrdvl7FiJNHJHCbZwWyhCvUX96RAnCBRFoUUYvIVIKG0Viyq3BIgW6BMzNsi1hcZk21G88YWWAnr39", + "+3SgmCVkMOZoc7OhBbMiRR0iXjiuSsC6Opp7YVwG56AXm408NbNdw8rmZ3XbNebewpfTlRmFg74R8ppI", + "UfCkryXLiVEdebzC2QAjcXjCliwpaOqGCUneQOrpO6iBm56xNyS5PlQZwSE7Uo98Sd4rGWFDOu46Ta+x", + "2VpqWM9sTcRVSWEfCF+BrLMHosnm6fM5dT1bf1EMXbGsSK1nteW6etbssFGxhSNrrhoaUd+NpndAk7Oa", + "aSu0W/eFrmbC6FAO/jLvs5sSz6kW39x5d82irZm5dMlrWfm6thNtg9372TROPhDphy2gh5I/Wj2dGyYm", + "ky2x8NUIrF+tQdYb1nfAV5mKOYym0i3kgTDUTvK8M3LuZf5ajoAQn1mPlSVTzL0k+NvyV4PxH1jiohXF", + "TT0RShPNzSTjYa0Pg7BRa0HfKC9QbTbUHhHutdpobtSnHzHTSm2fiHr4crKeIXXOlj4JpVVMU6AKULeq", + "5/bakr4zpPGUyWgfiDTb6dYPlBtmoK/kuERQqhQzFk0U8bBGMXPQlmAmZRWETiHxPehGOqCHPB7DeYfC", + "vIvBJXal5SLuYxe/B+1ZrTaFc2/zM+2ifDSz94c3t0xL9EBk3q4LcCft0O2CWdmXJfU3PttOAzv+VCx9", + "wipJo3bBWKNiwgY56lKaVPOg3ynKTF6K0sohzdrJK8/IWl6GMQ9lWxiQ1yh/DWASFsDtvbmd1qFHFMCY", + "G2DCqRkI1ZUZfc70YCYBElDXWuQDIefDW/O/XAothrcnJ/ZDnlLGh3awBGaDhZXnziFnIbiQqu530U9h", + "CdV6zY3auVvFbivQsU45E5rFgkiCLx4uV8gDsUOr0sWB3IAIRWr5mrQFe8bXbUlIlzsQviqd17tF1Xt6", + "DZWT+0NpjC1f/c8ORxtPHJbROQxzG1tSzbTdutk6WCoACA76RRF6RnN8kaSkQpB35tqCTle9JSzEbBQC", + "WTpP/XRltLehMLztowfMd7qm49UkaVNbbNj5GglvnBrYCANwKcU5ScUcvWY0i68VOeJCuxAVa+KsURCZ", + "woIumSFpuiJLKld/J7pAK52roOAZ2PtQTYVe1JZinxt9VALGMDjbpXvq7llpbsUbzmwFfNYw/5CjcgxU", + "hasJjq3fB1qR0NoIkLpgOScK/+UEuzNg9PuuMNbPpN+3HlgjYl8QrEJu3xD+FZKQVz4Y4IHYr17Q50Dp", + "6MjrK7EhWWAqXcGih2qjGe+hzfmMqh3C0fk7PhBe2tWA7mDksB6FX82phRXx0KjRjQVX2KThwRJwlXBZ", + "yx5KeQhk6fuDDRrN6jeB4+sXZ8HwlWBibOlTqN0Bzc9H327v1yxWeo9+AR3LMaQxU0Nb92lSJmNCMilC", + "1vhmbayHMsmHK3Ad+rpZBXLYdX5FrGtXSij6U1bb7/Fii0HtgBdbreqh8dIu5nWwzadEiV1icjfOer69", + "X7MG7r0YixDyesLzdbx5N4QNKHttXQG+bmxhmN6fAFGIjxJH4oangiaGuyYfGYajzEGHwp90IbkilPx2", + "cWnjbWreIzY/HaJL+ZtFLaSunmN+Df9u/ldM/sZy9HbxVTgxB9PORfu8S4vRoP2iMF2h6ffvAlAcWKcd", + "H1zYpIFe3ZNoW7Dih70OZ7evd7pQml33ayzjcJCw6hv8GOnSIasuQgj1hOaW3EGvSic7EKymcvBRaXKk", + "qay5PmXe8IK++2as4410PeYbCJv8pnRCxGwGUhHF5hzLiHCdrsiMKg2ynBCzSvFkzBOof2U+UwmYf+4j", + "y92FmMYLBkvM2Q56fRRko/CrR42rzB49FrbqfWpnIC2Xi9bBAfmBzRcg7V9lIQOiMpqmUKJXkWmhiabX", + "QFLB5yAHY963mFD6JfmPwbYdgpz0iAuqMYiFhBz959lo1H8xGpE33w3VsenoAoiaHZ/1yJSmlMdGlTI9", + "h4gBcvSfkxe1vhZxza5/63l8+i4vRv3/0ejUAvOkh9+WPZ6O+s/LHh0YqVHLBIeJ6uio8hf6T1UmGbdV", + "Ua/2mwUZP6hQXpx9paLj3juJxfeOt/8fE426uexSPBr5NfFxUU4sNkVDWdFkV5mwtWjM13DC7qcTVlVd", + "2gSFWl6tZMwjJJvvQTeK3vgchi3slWSTMqVRT1eddFPV3jnsMHmclFKtOkAq1fUttXF/j5BW0BMeMW+d", + "dNu0gdVauq5vvr7IAz4738fVDZ95K3PHI8QTrgArSmBswSZmlkCT8tId5OV3QBN35d6NlXEyrxKa8b8W", + "bhaxBt2vMufdSZdA0R/0kXxkxIIemeVVxnQsiUOBFfSTWsKeTu5u5016OAe/jgRNB0eu1fIROXe8R4jI", + "K9CBgnY11A0xl5NasLzEsA1d6X60xRhCH+GCkVo2LkNIYiOsUnAHgnODkZAJJwOsn+igI6LLqwf3FsJV", + "aiQdMViH1KeqZSRwCu1uFau8QN030slFOW0uQrU5Vh134d6inBBLZYDTYxd1gcCnmdPX6uzgTZsbAzgp", + "Gl6Q32zZBhurybSqbJst17BQ/bMQc1jr5r2xxr6kn9TTeNWiUMuLsxa78UE9sPAOUX+b+OFAwv6N5RVZ", + "1xD4pyFyWg8mXiPRFr0748oWgt/XNNrFF2O+nTG2m0gbFtExXzOJdocSOxvnvTGXt6oE63yvmV7KI2Qr", + "M/S+HNOaT/mkorvNSZGqpOIpWBUBD86qu038JVnuc6M62DBQGBNXGXLq97FNv+p3vC1R1pq88Hh4EHFx", + "6vbwTy4y1sm1Q2zcrAf7rt0EatklH+oOEEhguTtuD0xMhMsO1t74hbN/FxDKulhx5Y3bjq2J7Np3TVwm", + "ue/8GV+I2Oxi6kZqFwTN5zVNDHdr+Mlv+WeXoQ9sAOA6vYm8Irc1IwUaHpylwdkdSjxusj1sNzUESg94", + "RIk8f/yIusL0kWZFGE0fMB6tI2lo/U87TUm2dMRrdW6b/YG4WjcLabjVFtqgPWjbe8AVXm1tDYyQP/fV", + "ea0CQ3UXdv65mDmeJrjqT9E/+ldX530Xmtt/H6yh/wYSRl1Wwxkxw2NJB+fue7QuxI4bL3f+la4l6gKP", + "cp8fI5niRrd22YUTWrFbUqy5zG92MsKA110Mnq9qyhdtGT//wHfvMuvvrMwN3pkW3BfwRbXsm+fPu8DE", + "XNodYG1MJm6Zb5cT/47m2AOtGWW49WM/RtEsZU5O7w9ZuWqlYq6G1caGn+jE3JXy6ZDDawRhK6xvpFwv", + "aByJV7mjgqVlwtPMRJqKm7DnQaOeSi3j9zqaBU9XVUY8NvPV4ZkiDrQNjNl9quwzT23t4dmqBhNXkij6", + "YifaT2K+41FmCOurPr1CJ4MBGhMImqktg+QpXd1gKZKhSxGzQ+oiOWVaUrkil2VvV9aNG+7DivRVpQBE", + "za0mdE4ZV/YmPpXiRoEkrn7amAtOUhHTdCGUfvnt06dPbX5kHHVBFaGxr934JKdzeNIjT9y4T2xiqSdu", + "yCdVZV0XASXLumHaj1gBh2modCG5zfJcz2AUMpy4LajWfWZPh4e42bXm+kJRDwE4sHpbKC682tyvMdVQ", + "tQQM6blCyC1FBIjTMYiVScgd3Rf9Wl3TB4udbVdO/WPpoF3vOUABVaYw6dp8FSmmgsXdmwjGUqVbMYzl", + "UR8WxY3Kul8Gx/UisKGj0FZ1/cpwSzcg91NVL/bz8Jo1o3ODiP6RYZjn9nt5rRLtJpVwS5nZ3S8LByG0", + "Xub7q8oC9PbHR+lfYERJWafcq63dFCexRPhWmrOVxP88VNesqv7fdHd3B6XOSvMbiE+V5aOD199mkek/", + "mvYe+ByziwodYe6XR+mlXKvzbJfXjfqE7aDTYKs/jdRpVNX+QvpTrch1gPi+qxedfrQWt+rks1W4N9Oh", + "KPQ2Q1y1eaLQGy1yX0ge3cGyFCgZvtXGtFYM3Oi469XA//sB5QEeUGpULQq9ZjAri/YNq0fYsHS1kcNV", + "PeuHDNRuldXrztvUVZ7xi4Vof6HcFmVgdy5hyfDO6Ev01Sv+tbDugss6pZiPPqsjfuPrWfloVRYIrLwn", + "BgRTKonMHBXNTEmFz4PnXgXK7l0PWSj0ws9Y20oMbheNuGHDLH9+53CCWsFQ+/TYEHDlr/3XrlR+/3Rj", + "yXoxs8X0moU/fZ39Afm+oJJyDdZfbgrk3euzZ8+efTvY/ALSAOXK+qMcBInzZTkUEAPK09HTTYzNjCRj", + "aYp16KWYS1CqR3LMFUu0XFnbJ6bGl83tfgdarvqnMx0qiXxVzOc2VhRT1mJ1lVr50qqyiVxZJqgWsal6", + "6WM8N8qAU5vmSiEvArpo7iBRUmZPj874wXeOsdVdc7+W8QCbDhQ/m430bDnZt/jVF4WRJZT3FmBH07Q+", + "bHPbWtWFAq53D334hmsnB8/ek00s6oTAI8wQhTtQZkis5NqAvOXpCgMMKlmXgyQXr7C8COYNnDOlsQIK", + "poMzEmTQxrLINyG5VlH4wXAcqFq8v3rlXOG+bDI+LfLm8YML+b8BAAD//27HRaGCwQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/lib/typinghumanizer/typinghumanizer.go b/server/lib/typinghumanizer/typinghumanizer.go new file mode 100644 index 00000000..66b00790 --- /dev/null +++ b/server/lib/typinghumanizer/typinghumanizer.go @@ -0,0 +1,167 @@ +package typinghumanizer + +import ( + "math/rand" + "strings" + "time" + "unicode" +) + +// UniformJitter returns a random duration in [baseMs-jitterMs, baseMs+jitterMs], +// clamped to a minimum of minMs. +func UniformJitter(rng *rand.Rand, baseMs, jitterMs, minMs int) time.Duration { + ms := baseMs - jitterMs + rng.Intn(2*jitterMs+1) + if ms < minMs { + ms = minMs + } + return time.Duration(ms) * time.Millisecond +} + +// SplitWordChunks splits text into word-sized chunks, keeping trailing +// whitespace and punctuation attached to the preceding word. +// For example, "Hello world. How are you?" becomes: +// +// ["Hello ", "world. ", "How ", "are ", "you?"] +func SplitWordChunks(text string) []string { + if len(text) == 0 { + return nil + } + + var chunks []string + var current strings.Builder + + runes := []rune(text) + i := 0 + for i < len(runes) { + r := runes[i] + current.WriteRune(r) + i++ + + if unicode.IsSpace(r) { + for i < len(runes) && unicode.IsSpace(runes[i]) { + current.WriteRune(runes[i]) + i++ + } + chunks = append(chunks, current.String()) + current.Reset() + } + } + + if current.Len() > 0 { + chunks = append(chunks, current.String()) + } + + return chunks +} + +// IsSentenceEnd returns true if the chunk ends with sentence-ending punctuation +// (before any trailing whitespace). +func IsSentenceEnd(chunk string) bool { + trimmed := strings.TrimRightFunc(chunk, unicode.IsSpace) + if len(trimmed) == 0 { + return false + } + last := trimmed[len(trimmed)-1] + return last == '.' || last == '!' || last == '?' +} + +// TypoKind identifies the type of typo to inject. +type TypoKind int + +const ( + TypoAdjacentKey TypoKind = iota // Hit a neighboring key + TypoDoubling // Type the character twice + TypoTranspose // Swap current and next character + TypoExtraChar // Insert a random adjacent key before the correct one +) + +// Typo describes a single typo at a position in the text. +type Typo struct { + Pos int // Character index in the rune slice + Kind TypoKind // What kind of typo +} + +// qwertyAdj maps each lowercase letter to its adjacent keys on a QWERTY layout. +var qwertyAdj = [26][]byte{ + {'q', 'w', 's', 'z'}, // a + {'v', 'g', 'h', 'n'}, // b + {'x', 'd', 'f', 'v'}, // c + {'s', 'e', 'r', 'f', 'c', 'x'}, // d + {'w', 's', 'd', 'r'}, // e + {'d', 'r', 't', 'g', 'v', 'c'}, // f + {'f', 't', 'y', 'h', 'b', 'v'}, // g + {'g', 'y', 'u', 'j', 'n', 'b'}, // h + {'u', 'j', 'k', 'o'}, // i + {'h', 'u', 'i', 'k', 'n', 'm'}, // j + {'j', 'i', 'o', 'l', 'm'}, // k + {'k', 'o', 'p'}, // l + {'n', 'j', 'k'}, // m + {'b', 'h', 'j', 'm'}, // n + {'i', 'k', 'l', 'p'}, // o + {'o', 'l'}, // p + {'w', 'a'}, // q + {'e', 'd', 'f', 't'}, // r + {'a', 'w', 'e', 'd', 'x', 'z'}, // s + {'r', 'f', 'g', 'y'}, // t + {'y', 'h', 'j', 'i'}, // u + {'c', 'f', 'g', 'b'}, // v + {'q', 'a', 's', 'e'}, // w + {'z', 's', 'd', 'c'}, // x + {'t', 'g', 'h', 'u'}, // y + {'a', 's', 'x'}, // z +} + +// AdjacentKey returns a random QWERTY neighbor of the given character. +// If the character has no known neighbors (non-letter), it returns the +// character itself unchanged. +func AdjacentKey(rng *rand.Rand, ch rune) rune { + lower := unicode.ToLower(ch) + if lower < 'a' || lower > 'z' { + return ch + } + neighbors := qwertyAdj[lower-'a'] + if len(neighbors) == 0 { + return ch + } + adj := rune(neighbors[rng.Intn(len(neighbors))]) + if unicode.IsUpper(ch) { + adj = unicode.ToUpper(adj) + } + return adj +} + +// GenerateTypoPositions computes typo positions using geometric gap sampling. +// O(typos) random calls, not O(chars). Returns a sorted slice of Typo structs. +func GenerateTypoPositions(rng *rand.Rand, textLen int, typoRate float64) []Typo { + if typoRate <= 0 || textLen <= 1 { + return nil + } + avgGap := int(1.0 / typoRate) + if avgGap < 2 { + avgGap = 2 + } + + var typos []Typo + halfGap := avgGap / 2 + if halfGap < 1 { + halfGap = 1 + } + pos := halfGap + rng.Intn(avgGap) + for pos < textLen { + roll := rng.Intn(100) + var kind TypoKind + switch { + case roll < 60: + kind = TypoAdjacentKey + case roll < 80: + kind = TypoDoubling + case roll < 95: + kind = TypoTranspose + default: + kind = TypoExtraChar + } + typos = append(typos, Typo{Pos: pos, Kind: kind}) + pos += halfGap + rng.Intn(avgGap) + } + return typos +} diff --git a/server/lib/typinghumanizer/typinghumanizer_test.go b/server/lib/typinghumanizer/typinghumanizer_test.go new file mode 100644 index 00000000..5271aa80 --- /dev/null +++ b/server/lib/typinghumanizer/typinghumanizer_test.go @@ -0,0 +1,192 @@ +package typinghumanizer + +import ( + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUniformJitter(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + + t.Run("stays within range", func(t *testing.T) { + for i := 0; i < 1000; i++ { + d := UniformJitter(rng, 100, 30, 50) + ms := d.Milliseconds() + assert.GreaterOrEqual(t, ms, int64(50), "should be >= minMs") + assert.LessOrEqual(t, ms, int64(130), "should be <= baseMs+jitterMs") + } + }) + + t.Run("clamps to minimum", func(t *testing.T) { + for i := 0; i < 100; i++ { + d := UniformJitter(rng, 10, 20, 5) + assert.GreaterOrEqual(t, d.Milliseconds(), int64(5)) + } + }) + + t.Run("zero jitter returns base", func(t *testing.T) { + d := UniformJitter(rng, 100, 0, 0) + assert.Equal(t, 100*time.Millisecond, d) + }) +} + +func TestSplitWordChunks(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "simple sentence", + input: "Hello world", + expected: []string{"Hello ", "world"}, + }, + { + name: "with punctuation", + input: "Hello world. How are you?", + expected: []string{"Hello ", "world. ", "How ", "are ", "you?"}, + }, + { + name: "single word", + input: "Hello", + expected: []string{"Hello"}, + }, + { + name: "empty string", + input: "", + expected: nil, + }, + { + name: "only spaces", + input: " ", + expected: []string{" "}, + }, + { + name: "multiple spaces between words", + input: "Hello world", + expected: []string{"Hello ", "world"}, + }, + { + name: "trailing space", + input: "Hello ", + expected: []string{"Hello "}, + }, + { + name: "leading space", + input: " Hello", + expected: []string{" ", "Hello"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := SplitWordChunks(tt.input) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestIsSentenceEnd(t *testing.T) { + tests := []struct { + chunk string + expected bool + }{ + {"world. ", true}, + {"you?", true}, + {"wow! ", true}, + {"Hello ", false}, + {"word", false}, + {"", false}, + {" ", false}, + {"end.", true}, + } + + for _, tt := range tests { + t.Run(tt.chunk, func(t *testing.T) { + assert.Equal(t, tt.expected, IsSentenceEnd(tt.chunk)) + }) + } +} + +func TestAdjacentKey(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + + t.Run("returns a neighbor for lowercase letters", func(t *testing.T) { + for i := 0; i < 100; i++ { + adj := AdjacentKey(rng, 'a') + assert.Contains(t, []rune{'q', 'w', 's', 'z'}, adj) + } + }) + + t.Run("preserves uppercase", func(t *testing.T) { + for i := 0; i < 100; i++ { + adj := AdjacentKey(rng, 'A') + assert.Contains(t, []rune{'Q', 'W', 'S', 'Z'}, adj) + } + }) + + t.Run("returns same char for non-letters", func(t *testing.T) { + assert.Equal(t, '5', AdjacentKey(rng, '5')) + assert.Equal(t, '.', AdjacentKey(rng, '.')) + assert.Equal(t, ' ', AdjacentKey(rng, ' ')) + }) +} + +func TestGenerateTypoPositions(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + + t.Run("zero rate returns nil", func(t *testing.T) { + assert.Nil(t, GenerateTypoPositions(rng, 100, 0)) + }) + + t.Run("short text returns nil or few typos", func(t *testing.T) { + typos := GenerateTypoPositions(rng, 1, 0.05) + assert.Nil(t, typos) + }) + + t.Run("positions are within bounds and sorted", func(t *testing.T) { + textLen := 200 + typos := GenerateTypoPositions(rng, textLen, 0.03) + for i, typo := range typos { + assert.GreaterOrEqual(t, typo.Pos, 0) + assert.Less(t, typo.Pos, textLen) + if i > 0 { + assert.Greater(t, typo.Pos, typos[i-1].Pos, "positions must be strictly increasing") + } + } + }) + + t.Run("roughly matches expected count", func(t *testing.T) { + textLen := 1000 + rate := 0.03 + totalTypos := 0 + runs := 200 + for i := 0; i < runs; i++ { + localRng := rand.New(rand.NewSource(int64(i))) + typos := GenerateTypoPositions(localRng, textLen, rate) + totalTypos += len(typos) + } + avgTypos := float64(totalTypos) / float64(runs) + expected := float64(textLen) * rate + assert.InDelta(t, expected, avgTypos, expected*0.3, "average typo count should be near expected") + }) + + t.Run("kind distribution is weighted", func(t *testing.T) { + counts := map[TypoKind]int{} + for i := 0; i < 500; i++ { + localRng := rand.New(rand.NewSource(int64(i))) + typos := GenerateTypoPositions(localRng, 500, 0.05) + for _, typo := range typos { + counts[typo.Kind]++ + } + } + total := counts[TypoAdjacentKey] + counts[TypoDoubling] + counts[TypoTranspose] + counts[TypoExtraChar] + require.Greater(t, total, 0) + adjPct := float64(counts[TypoAdjacentKey]) / float64(total) + assert.InDelta(t, 0.60, adjPct, 0.10, "adjacent key should be ~60%%") + }) +} diff --git a/server/openapi.yaml b/server/openapi.yaml index 79396d80..737e1db6 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1367,9 +1367,26 @@ components: description: Text to type on the host computer delay: type: integer - description: Delay in milliseconds between keystrokes + description: Delay in milliseconds between keystrokes. Ignored when smooth is true. minimum: 0 default: 0 + smooth: + type: boolean + description: | + Use human-like variable keystroke timing instead of a fixed delay. + When enabled, text is typed in word-sized chunks with variable + intra-word delays and natural inter-word pauses. The delay field + is ignored when smooth is true. + default: false + typo_chance: + type: number + description: | + Probability (0.0-0.10) of a typo per character, which is then + corrected with backspace. Requires smooth to be true. Set to 0 + to disable. Typical human range is 0.02-0.05. + minimum: 0 + maximum: 0.10 + default: 0 additionalProperties: false ClipboardContent: type: object From 025b75a4a78ce3b5072f6f515a4da22c2df367e6 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Fri, 3 Apr 2026 11:13:01 -0600 Subject: [PATCH 2/3] Add smooth typing demo page and recording script - typing_demo.html: Dark-themed page with a textarea and keystroke timing visualization (bar chart of inter-key intervals) - demo_smooth_typing.py: Records three phases (instant, smooth, smooth+typos) via the kernel API for comparison GIF/MP4 Made-with: Cursor --- .../demos/demo_smooth_typing.py | 137 ++++++++++++++ .../chromium-headful/demos/typing_demo.html | 170 ++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 images/chromium-headful/demos/demo_smooth_typing.py create mode 100644 images/chromium-headful/demos/typing_demo.html diff --git a/images/chromium-headful/demos/demo_smooth_typing.py b/images/chromium-headful/demos/demo_smooth_typing.py new file mode 100644 index 00000000..14502073 --- /dev/null +++ b/images/chromium-headful/demos/demo_smooth_typing.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +Demo script: smooth typing vs instant typing. + +Drives the typing_demo.html page through the kernel-images API to produce +a side-by-side comparison suitable for recording as a GIF/MP4. + +Usage: + # 1. Start a kernel-images container + # 2. Upload typing_demo.html to the container + # 3. Run this script: + python demo_smooth_typing.py --base-url http://localhost:8000 + +Requirements: + pip install requests +""" + +import argparse +import base64 +import json +import time +from pathlib import Path + +import requests + +DEMO_TEXT = "The quick brown fox jumps over the lazy dog. Hello world!" + + +def api(base: str, method: str, path: str, **kwargs): + url = f"{base}{path}" + resp = getattr(requests, method)(url, **kwargs) + resp.raise_for_status() + return resp + + +def upload_demo_page(base: str): + html_path = Path(__file__).parent / "typing_demo.html" + html_bytes = html_path.read_bytes() + api(base, "put", "/fs/write_file", params={"path": "/tmp/typing_demo.html"}, + data=html_bytes, headers={"Content-Type": "application/octet-stream"}) + print("Uploaded typing_demo.html") + + +def execute_js(base: str, code: str): + api(base, "post", "/playwright/execute", json={"code": code, "timeout_sec": 10}) + + +def navigate(base: str): + execute_js(base, "await page.goto('file:///tmp/typing_demo.html');") + time.sleep(1) + + +def click_input(base: str): + execute_js(base, "await page.click('#input');") + time.sleep(0.3) + + +def clear_input(base: str): + execute_js(base, "window.demoApi.clear();") + time.sleep(0.3) + + +def set_mode(base: str, label: str, cls: str): + execute_js(base, f"window.demoApi.setMode('{label}', '{cls}');") + + +def type_text(base: str, text: str, smooth: bool = False, typo_chance: float = 0): + body = {"text": text, "smooth": smooth} + if typo_chance > 0: + body["typo_chance"] = typo_chance + if not smooth: + body["delay"] = 0 + api(base, "post", "/computer/type", json=body) + + +def start_recording(base: str): + api(base, "post", "/recording/start", json={"framerate": 15, "id": "typing-demo"}) + print("Recording started") + time.sleep(0.5) + + +def stop_recording(base: str): + api(base, "post", "/recording/stop", json={"id": "typing-demo"}) + time.sleep(1) + print("Recording stopped") + + +def download_recording(base: str, output: str): + resp = api(base, "get", "/recording/download", params={"id": "typing-demo"}) + Path(output).write_bytes(resp.content) + print(f"Saved recording to {output}") + + +def run_demo(base: str, output: str): + upload_demo_page(base) + navigate(base) + start_recording(base) + + # --- Phase 1: Instant typing (no delay) --- + set_mode(base, "INSTANT TYPING — delay: 0", "instant") + time.sleep(1) + click_input(base) + type_text(base, DEMO_TEXT, smooth=False) + time.sleep(2) + + # --- Phase 2: Smooth typing (no typos) --- + clear_input(base) + set_mode(base, "SMOOTH TYPING — HUMAN-LIKE", "smooth") + time.sleep(1) + click_input(base) + type_text(base, DEMO_TEXT, smooth=True) + time.sleep(2) + + # --- Phase 3: Smooth typing with typos --- + clear_input(base) + set_mode(base, "SMOOTH TYPING — WITH TYPOS", "typos") + time.sleep(1) + click_input(base) + type_text(base, DEMO_TEXT, smooth=True, typo_chance=0.04) + time.sleep(2) + + stop_recording(base) + download_recording(base, output) + + +def main(): + parser = argparse.ArgumentParser(description="Smooth typing demo recorder") + parser.add_argument("--base-url", default="http://localhost:8000", + help="Base URL of the kernel-images API") + parser.add_argument("--output", default="smooth_typing_demo.mp4", + help="Output video file path") + args = parser.parse_args() + run_demo(args.base_url, args.output) + + +if __name__ == "__main__": + main() diff --git a/images/chromium-headful/demos/typing_demo.html b/images/chromium-headful/demos/typing_demo.html new file mode 100644 index 00000000..3d4cf4be --- /dev/null +++ b/images/chromium-headful/demos/typing_demo.html @@ -0,0 +1,170 @@ + + + + + + Typing Demo + + + + + +
+
Input
+ +
+ +
+ +
+ +
Keystroke timing visualization
+ + + + From 99e2e79da37cf943ad3caae4c579c8a29f8e9006 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Mon, 6 Apr 2026 12:42:23 -0600 Subject: [PATCH 3/3] Remove redundant if/else in typeChunkWithTypo Both branches produced identical correctText. Simplified to a single expression as suggested by jarugupj. Made-with: Cursor --- server/cmd/api/api/computer.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index b5f00dde..8ed937cd 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -633,14 +633,7 @@ func (s *ApiService) typeChunkWithTypo( } // Type the correct remainder of the chunk from the typo position onward - var correctText string - if typo.Kind == typinghumanizer.TypoTranspose && typoLocalPos+1 < len(chunkRunes) { - correctText = string(chunkRunes[typoLocalPos:]) - } else { - correctText = string(chunkRunes[typoLocalPos:]) - } - - if err := s.xdotoolTypeChunk(ctx, correctText, delayMs); err != nil { + if err := s.xdotoolTypeChunk(ctx, string(chunkRunes[typoLocalPos:]), delayMs); err != nil { return &executionError{msg: "failed during smooth typing"} }