diff --git a/go.mod b/go.mod index 1bfffc2..bd54c89 100644 --- a/go.mod +++ b/go.mod @@ -8,15 +8,20 @@ require ( github.com/galeone/tfgo v0.0.0-20230715013254-16113111dc99 github.com/google/uuid v1.6.0 github.com/lib/pq v1.10.9 - golang.org/x/crypto v0.18.0 + golang.org/x/crypto v0.19.0 ) require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/lipgloss v0.9.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -24,6 +29,8 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.6 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.32.0 // indirect ) diff --git a/go.sum b/go.sum index e170f8b..ff10af9 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/charmbracelet/log v0.2.5 h1:1yVvyKCKVV639RR4LIq1iy1Cs1AKxuNO+Hx2LJtk7 github.com/charmbracelet/log v0.2.5/go.mod h1:nQGK8tvc4pS9cvVEH/pWJiZ50eUq1aoXUOjGpXvdD0k= github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw= github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/galeone/tensorflow/tensorflow/go v0.0.0-20221023090153-6b7fa0680c3e h1:9+2AEFZymTi25FIIcDwuzcOPH04z9+fV6XeLiGORPDI= github.com/galeone/tensorflow/tensorflow/go v0.0.0-20221023090153-6b7fa0680c3e/go.mod h1:TelZuq26kz2jysARBwOrTv16629hyUsHmIoj54QqyFo= github.com/galeone/tensorflow/tensorflow/go v0.0.0-20240119075110-6ad3cf65adfe h1:7yELf1NFEwECpXMGowkoftcInMlVtLTCdwWLmxKgzNM= @@ -18,6 +20,12 @@ github.com/galeone/tfgo v0.0.0-20230715013254-16113111dc99 h1:8Bt1P/zy1gb37L4n8C github.com/galeone/tfgo v0.0.0-20230715013254-16113111dc99/go.mod h1:3YgYBeIX42t83uP27Bd4bSMxTnQhSbxl0pYSkCDB1tc= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= +github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -26,6 +34,8 @@ github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -52,10 +62,14 @@ golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -63,6 +77,10 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= diff --git a/logic/models/data.go b/logic/models/data.go index 0212af4..1d03c3d 100644 --- a/logic/models/data.go +++ b/logic/models/data.go @@ -309,58 +309,69 @@ func processZipFileExpand(c *Context, model *BaseModel) { ids[name] = id } + back_channel := make(chan int, c.Handle.Config.NumberOfWorkers) + + file_chans := make([]chan *zip.File, c.Handle.Config.NumberOfWorkers) + + for i := 0; i < c.Handle.Config.NumberOfWorkers; i++ { + file_chans[i] = make(chan *zip.File, 2) + go fileProcessor(c, model, reader, ids, base_path, i, file_chans[i], back_channel) + } + + clean_up_channels := func() { + for i := 0; i < c.Handle.Config.NumberOfWorkers; i++ { + close(file_chans[i]) + } + for i := 0; i < c.Handle.Config.NumberOfWorkers - 1; i++ { + _ = <- back_channel + } + close(back_channel) + } + + first_round := true + + channel_to_send := 0 + + // Parelalize this + for _, file := range reader.Reader.File { + // Skip if dir if file.Name[len(file.Name)-1] == '/' { continue } - data, err := reader.Open(file.Name) - if err != nil { - failed(fmt.Sprintf("Could not open file in zip %s\n", file.Name)) - return - } - defer data.Close() - file_data, err := io.ReadAll(data) - if err != nil { - failed(fmt.Sprintf("Could not read file file in zip %s\n", file.Name)) - return - } + file_chans[channel_to_send] <- file - // TODO check if the file is a valid photo that matched the defined photo on the database - parts := strings.Split(file.Name, "/") + if first_round { + channel_to_send += 1 + if c.Handle.Config.NumberOfWorkers == channel_to_send { + first_round = false + } + } + + // Can not do else if because need to handle the case where the value changes in + // previous if + if !first_round { + new_id, ok := <- back_channel + if !ok { + c.Logger.Fatal("Something is very wrong please check as this line should be unreachable") + } - mode := model_classes.DATA_POINT_MODE_TRAINING - if parts[0] == "testing" { - mode = model_classes.DATA_POINT_MODE_TESTING - } + if new_id < 0 { + c.Logger.Error("Worker failed", "worker id", -(new_id + 1)) + clean_up_channels() + failed("One of the workers failed due to db error") + return + } - data_point_id, err := model_classes.AddDataPoint(c.Db, ids[parts[1]], "id://", mode) - if err != nil { - failed(fmt.Sprintf("Failed to add data point for %s\n", model.Id)) - return - } + channel_to_send = new_id + } - file_path := path.Join(base_path, data_point_id+"."+model.Format) - f, err := os.Create(file_path) - if err != nil { - failed(fmt.Sprintf("Could not create file %s\n", file_path)) - return - } - defer f.Close() - f.Write(file_data) - - if !testImgForModel(c, model, file_path) { - c.Logger.Errorf("Image did not have valid format for model %s (in zip: %s)!", file_path, file.Name) - c.Logger.Warn("Not failling updating data point to status -1") - message := "Image did not have valid format for the model" - if err = model_classes.UpdateDataPointStatus(c.Db, data_point_id, -1, &message); err != nil { - failed(fmt.Sprintf("Failed to update data point status")) - return - } - } } + clean_up_channels() + c.Logger.Info("Added data to model", "id", model.Id) ModelUpdateStatus(c, model.Id, READY) } diff --git a/logic/models/list.go b/logic/models/list.go index 8094217..2a5c4c9 100644 --- a/logic/models/list.go +++ b/logic/models/list.go @@ -5,6 +5,31 @@ import ( . "git.andr3h3nriqu3s.com/andr3/fyp/logic/utils" ) +// Auth level set when path is definied as 1 +func handleStats(c *Context) *Error { + var b struct { + Id string `json:"id" validate:"required"` + } + + if _err := c.ToJSON(&b); _err != nil { + return _err; + } + + type Row struct { + Name string `db:"mc.name" json:"name"` + Training string `db:"count(mdp.id) filter (where mdp.model_mode=1)" json:"training"` + Testing string `db:"count(mdp.id) filter (where mdp.model_mode=2)" json:"testing"` + } + + rows, err := GetDbMultitple[Row](c, "model_data_point as mdp inner join model_classes as mc on mc.id=mdp.class_id where mc.model_id=$1 group by mc.name order by mc.name asc;", b.Id) + if err != nil { + return c.Error500(err) + } + + c.ShowMessage = false + return c.SendJSON(rows) +} + func handleList(handle *Handle) { handle.Get("/models", func(c *Context) *Error { if !c.CheckAuthLevel(1) { @@ -20,7 +45,10 @@ func handleList(handle *Handle) { if err != nil { return c.Error500(nil) } - + + c.ShowMessage = true return c.SendJSON(got) }) + + handle.PostAuth("/models/class/stats", 1, handleStats) } diff --git a/logic/utils/handler.go b/logic/utils/handler.go index 94c9edc..5e49567 100644 --- a/logic/utils/handler.go +++ b/logic/utils/handler.go @@ -15,6 +15,7 @@ import ( dbtypes "git.andr3h3nriqu3s.com/andr3/fyp/logic/db_types" . "git.andr3h3nriqu3s.com/andr3/fyp/logic/models/utils" "github.com/charmbracelet/log" + "github.com/go-playground/validator/v10" "github.com/goccy/go-json" ) @@ -43,6 +44,7 @@ type Handle struct { posts []HandleFunc deletes []HandleFunc Config Config + validate *validator.Validate } func decodeBody(r *http.Request) (string, *Error) { @@ -79,6 +81,17 @@ func (x *Handle) Post(path string, fn func(c *Context) *Error) { x.posts = append(x.posts, HandleFunc{path, fn}) } + +func (x *Handle) PostAuth(path string, authLevel int, fn func(c *Context) *Error) { + inner_fn := func(c *Context) *Error { + if !c.CheckAuthLevel(authLevel) { + return nil + } + return fn(c) + } + x.posts = append(x.posts, HandleFunc{path, inner_fn}) +} + func (x *Handle) Delete(path string, fn func(c *Context) *Error) { x.deletes = append(x.deletes, HandleFunc{path, fn}) } @@ -195,6 +208,12 @@ func (c Context) ToJSON(dat any) *Error { return c.Error500(err) } + err = c.Handle.validate.Struct(dat) + if err != nil { + c.Logger.Error("Failed invalid json passed", "dat", dat, "err", err) + return c.JsonBadRequest("Bad Request! Invalid body passed!") + } + return nil } @@ -476,7 +495,8 @@ func NewHandler(db *sql.DB, config Config) *Handle { var gets []HandleFunc var posts []HandleFunc var deletes []HandleFunc - x := &Handle{db, gets, posts, deletes, config} + validate := validator.New() + x := &Handle{db, gets, posts, deletes, config, validate} http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { diff --git a/webpage/bun.lockb b/webpage/bun.lockb index 3db0581..b93f148 100755 Binary files a/webpage/bun.lockb and b/webpage/bun.lockb differ diff --git a/webpage/package.json b/webpage/package.json index 39ec920..6fbfc05 100644 --- a/webpage/package.json +++ b/webpage/package.json @@ -3,7 +3,8 @@ "version": "0.0.1", "private": true, "scripts": { - "dev": "vite dev", + "dev:raw": "vite dev", + "dev": "vite dev --port 5001 --host", "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", @@ -30,5 +31,9 @@ "typescript": "^5.0.0", "vite": "^5.0.3" }, - "type": "module" + "type": "module", + "dependencies": { + "chart.js": "^4.4.2", + "svelte-chartjs": "^3.1.5" + } } diff --git a/webpage/src/lib/Tabs.svelte b/webpage/src/lib/Tabs.svelte index 2bffee5..825065a 100644 --- a/webpage/src/lib/Tabs.svelte +++ b/webpage/src/lib/Tabs.svelte @@ -1,5 +1,5 @@ -
+
@@ -58,5 +58,16 @@ display: block; box-shadow: 0 2px 2px 1px #66666655; } + + &.nobox { + box-shadow: none; + overflow: visible; + + + :global(.content.selected) { + box-shadow: none; + } + } + } diff --git a/webpage/src/routes/models/edit/+page.svelte b/webpage/src/routes/models/edit/+page.svelte index 86348e1..52b1ee4 100644 --- a/webpage/src/routes/models/edit/+page.svelte +++ b/webpage/src/routes/models/edit/+page.svelte @@ -6,8 +6,8 @@ width: number; height: number; status: number; - model_type: number; - format: string; + model_type: number; + format: string; }; export type Layer = { @@ -25,85 +25,88 @@ @@ -127,186 +130,208 @@ -->
- {#await model} - Loading - {:then m} - {#if m.status == 1} -
-

- {m.name} -

- -

Preparing the model

-
- {:else if m.status == -1} -
-

- {m.name} -

- -

Failed to prepare model

+ +
+ + {#if _model && [2, 3, 4, 5, 6, 7].includes(_model.status)} + + {/if} +
+ {#if _model} + + {/if} +
+ {#await model} + Loading + {:then m} + {#if m.status == 1} +
+

+ {m.name} +

+ +

Preparing the model

+
+ {:else if m.status == -1} +
+

+ {m.name} +

+ +

Failed to prepare model

- -
- - {:else if m.status == 2} - - - - - {:else if m.status == -2} - - - - {:else if m.status == 3} - -
- - Processing zip file... -
- {:else if m.status == -3 || m.status == -4} - -
- Failed Prepare for training.
-
- - - - - {:else if m.status == 4} - - -
- - Training the model...
- - {#await definitions} - Loading - {:then defs} - - - - - - - - - - - {#each defs as def} - - - - - - - {#if def.status == 3 && def.layers} + + + + {:else if m.status == 2} + + + + + {:else if m.status == -2} + + + + {:else if m.status == 3} + +
+ + Processing zip file... +
+ {:else if m.status == -3 || m.status == -4} + +
+ Failed Prepare for training.
+
+ + + + + {:else if m.status == 4} + + +
+ + Training the model...
+ + {#await definitions} + Loading + {:then defs} +
Done Progress Training Round Progress Accuracy Status
- {def.epoch} - - {def.epoch_progress}/20 - - {def.accuracy}% - - {#if def.status == 2} - - {:else if [3, 6, -3].includes(def.status)} - - {:else} - {def.status} - {/if} -
+ - + + + + - {/if} - {/each} - -
- - {#each def.layers as layer, i} - {@const sep_mod = - def.layers.length > 8 - ? Math.max(10, 100 - (def.layers.length - 8) * 10) - : 100} - {#if layer.layer_type == 1} - - {:else if layer.layer_type == 4} - - - {:else if layer.layer_type == 3} - - - {:else if layer.layer_type == 2} - - - {:else} -
- {layer.layer_type} - {layer.shape} -
- {/if} - {/each} - -
Done Progress Training Round Progress Accuracy Status
- {/await} - -
- {:else if [5, 6, -6, 7, -7].includes(m.status)} - - - {#if m.status == 6} -
- Model expading... Processing ZIP file -
- {/if} - {#if m.status == -6} - - {/if} - {#if m.status == -7} -
- - Failed to train the model! - Try to retrain -
- {/if} - {#if m.model_type == 2} - - {/if} - - {:else} -

Unknown Status of the model.

- {/if} - {/await} + + + {#each defs as def} + + + {def.epoch} + + + {def.epoch_progress}/20 + + + {def.accuracy}% + + + {#if def.status == 2} + + {:else if [3, 6, -3].includes(def.status)} + + {:else} + {def.status} + {/if} + + + {#if def.status == 3 && def.layers} + + + + {#each def.layers as layer, i} + {@const sep_mod = + def.layers.length > 8 + ? Math.max(10, 100 - (def.layers.length - 8) * 10) + : 100} + {#if layer.layer_type == 1} + + {:else if layer.layer_type == 4} + + + {:else if layer.layer_type == 3} + + + {:else if layer.layer_type == 2} + + + {:else} +
+ {layer.layer_type} + {layer.shape} +
+ {/if} + {/each} + + + + {/if} + {/each} + + + {/await} + +
+ {:else if [5, 6, -6, 7, -7].includes(m.status)} + + + {#if m.status == 6} +
Model expading... Processing ZIP file
+ {/if} + {#if m.status == -6} + + {/if} + {#if m.status == -7} +
+ + Failed to train the model! Try to retrain +
+ {/if} + {#if m.model_type == 2} + + {/if} + + {:else} +

Unknown Status of the model.

+ {/if} + {/await} +
+
diff --git a/webpage/src/routes/models/edit/ModelTable.svelte b/webpage/src/routes/models/edit/ModelTable.svelte index fa802b9..e831dde 100644 --- a/webpage/src/routes/models/edit/ModelTable.svelte +++ b/webpage/src/routes/models/edit/ModelTable.svelte @@ -44,8 +44,6 @@ let res = await get('models/data/list?' + url.toString()); showNext = res.showNext; image_list = res.image_list; - - console.log(image_list); } catch (e) { console.error('TODO notify user', e); } diff --git a/webpage/src/routes/models/edit/types.ts b/webpage/src/routes/models/edit/types.ts new file mode 100644 index 0000000..92bee0e --- /dev/null +++ b/webpage/src/routes/models/edit/types.ts @@ -0,0 +1,5 @@ +export type ModelStats = Array<{ + name: string, + training: number, + testing: number, +}>