diff --git a/.vscode/settings.json b/.vscode/settings.json index 07a9450..9013e6f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,9 @@ { "cSpell.words": [ "Ведите", + "навык", "Навыки", + "skils", "typedataset" ] } diff --git a/README.md b/README.md index 13118e8..c0f7be8 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ git clone https://gitlab.com/robossembler/webservice Для работы Генератора Датасетов нужно задать следующие переменные в окружении `bash` ```bash -export PYTHON_BLENDER="путь_к_директории_с_файлами_из_rcg_pipeline" -export PYTHON_BLENDER_PROC="путь_к_генератору_датасетов_renderBOPdataset.py" +export PYTHON_BLENDER="/путь_к_директории_с_файлами_из/rcg_pipeline" +export PYTHON_BLENDER_PROC="/путь_к_генератору_датасетов_/renderBOPdataset.py" +export PYTHON_EDUCATION="absolute_path/webp/education.py" ``` ## Запуск сервера diff --git a/server/src/core/controllers/routes.ts b/server/src/core/controllers/routes.ts index 1710aae..32b4e44 100644 --- a/server/src/core/controllers/routes.ts +++ b/server/src/core/controllers/routes.ts @@ -1,9 +1,15 @@ import { BehaviorTreesPresentation } from "../../features/behavior_trees/behavior_trees"; import { DatasetsPresentation } from "../../features/datasets/datasets_presentation"; +import { WeightsPresentation } from "../../features/weights/weights_presentation"; import { ProjectsPresentation } from "../../features/projects/projects_presentation"; import { extensions } from "../extensions/extensions"; import { Routes } from "../interfaces/router"; extensions(); -export const httpRoutes: Routes[] = [new ProjectsPresentation(), new DatasetsPresentation(), new BehaviorTreesPresentation()].map((el) => el.call()); +export const httpRoutes: Routes[] = [ + new ProjectsPresentation(), + new DatasetsPresentation(), + new BehaviorTreesPresentation(), + new WeightsPresentation(), +].map((el) => el.call()); diff --git a/server/src/features/datasets/domain/create_dataset_scenario.ts b/server/src/features/datasets/domain/create_dataset_scenario.ts index 859c80c..22fe73a 100644 --- a/server/src/features/datasets/domain/create_dataset_scenario.ts +++ b/server/src/features/datasets/domain/create_dataset_scenario.ts @@ -9,11 +9,15 @@ import { IProjectModel, ProjectDBModel } from "../../_projects/models/project_da import { DatasetDBModel } from "../models/dataset_database_model"; import { DatasetValidationModel, ProcessStatus } from "../models/dataset_validation_model"; -export class ProcessWatcherAndDatabaseUpdateService extends TypedEvent> { +export class ProcessWatcherAndDatabaseUpdateService extends TypedEvent< + Result +> { databaseId: ObjectId; - constructor(databaseId: ObjectId) { + model: A; + constructor(databaseId: ObjectId, model: A) { super(); this.databaseId = databaseId; + this.model = model; this.on((event) => this.lister(event)); } @@ -21,7 +25,9 @@ export class ProcessWatcherAndDatabaseUpdateService extends TypedEvent { if (success.event == EXEC_EVENT.END) { - const dbModel = await DatasetDBModel.findById(this.databaseId); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const dbModel = await this.model.findById(this.databaseId); if (dbModel !== null) { dbModel.local_path; dbModel.processStatus = ProcessStatus.END; @@ -31,7 +37,9 @@ export class ProcessWatcherAndDatabaseUpdateService extends TypedEvent { - const dbModel = await DatasetDBModel.findById(this.databaseId); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const dbModel = await this.model.findById(this.databaseId); if (dbModel !== null) { dbModel.processStatus = ProcessStatus.ERROR; dbModel.processLogs = error.message; diff --git a/server/src/features/datasets/domain/exec_process_scenario.ts b/server/src/features/datasets/domain/exec_process_scenario.ts index 20062cf..e234315 100644 --- a/server/src/features/datasets/domain/exec_process_scenario.ts +++ b/server/src/features/datasets/domain/exec_process_scenario.ts @@ -14,14 +14,13 @@ export class ExecDatasetProcessScenario extends CallbackStrategyWithIdQuery { return (await new ReadByIdDataBaseModelUseCase(DatasetDBModel).call(id)).map(async (model) => { return (await new IsHaveActiveProcessUseCase().call()).map(async () => { await DatasetDBModel.findById(id).updateOne({ processStatus: "RUN" }); - console.log(`blenderproc run $PYTHON_BLENDER_PROC --cfg '${JSON.stringify(model)}'`); return new ExecProcessUseCase().call( `${model.project.rootDir}/`, `blenderproc run $PYTHON_BLENDER_PROC --cfg '${JSON.stringify(model)}'`, id, - new ProcessWatcherAndDatabaseUpdateService(id as unknown as ObjectId) + new ProcessWatcherAndDatabaseUpdateService(id as unknown as ObjectId, DatasetDBModel) ); }); }); }; -} +} diff --git a/server/src/features/datasets/models/dataset_database_model.ts b/server/src/features/datasets/models/dataset_database_model.ts index a169a3d..3903591 100644 --- a/server/src/features/datasets/models/dataset_database_model.ts +++ b/server/src/features/datasets/models/dataset_database_model.ts @@ -1,7 +1,7 @@ -import { Mongoose, Schema, model } from "mongoose"; +import { Schema, model } from "mongoose"; import { IDatasetModel } from "./dataset_validation_model"; import { projectSchema } from "../../_projects/models/project_database_model"; - + export const DatasetSchema = new Schema({ name: { type: String, diff --git a/server/src/features/weights/domain/exec_weights_process_scenario.ts b/server/src/features/weights/domain/exec_weights_process_scenario.ts new file mode 100644 index 0000000..300d265 --- /dev/null +++ b/server/src/features/weights/domain/exec_weights_process_scenario.ts @@ -0,0 +1,32 @@ +import { ObjectId } from "mongoose"; +import { CallbackStrategyWithIdQuery, ResponseBase } from "../../../core/controllers/http_controller"; +import { Result } from "../../../core/helpers/result"; +import { ExecProcessUseCase, IsHaveActiveProcessUseCase } from "../../../core/usecases/exec_process_usecase"; +import { ReadByIdDataBaseModelUseCase } from "../../../core/usecases/read_by_id_database_model_usecase"; +import { MongoIdValidation } from "../../../core/validations/mongo_id_validation"; +import { ProcessWatcherAndDatabaseUpdateService } from "../../datasets/domain/create_dataset_scenario"; +import { WeightDBModel, IWeightModel } from "../models/weights_validation_model"; + +export class ExecWeightProcessScenario extends CallbackStrategyWithIdQuery { + idValidationExpression = new MongoIdValidation(); + call = async (id: string): ResponseBase => { + return (await new ReadByIdDataBaseModelUseCase(WeightDBModel).call(id)).map(async (model) => { + return (await new IsHaveActiveProcessUseCase().call()).map(async () => { + await WeightDBModel.findById(id).updateOne({ processStatus: "RUN" }); + + if (typeof model.project === "object" && typeof model.datasetId === "object") { + console.log( + `python3 $PYTHON_EDUCATION --path ${model.project.rootDir} --name ${model.name} --datasetName ${model.datasetId.name}` + ); + return new ExecProcessUseCase().call( + `${model.project.rootDir}/`, + `python3 $PYTHON_EDUCATION --path ${model.project.rootDir} --name ${model.name} --datasetName ${model.datasetId.name}`, + id, + new ProcessWatcherAndDatabaseUpdateService(id as unknown as ObjectId, WeightDBModel) + ); + } + return Result.error("model project is not object"); + }); + }); + }; +} diff --git a/server/src/features/weights/models/weights_database_model.ts b/server/src/features/weights/models/weights_database_model.ts new file mode 100644 index 0000000..8d0c6dd --- /dev/null +++ b/server/src/features/weights/models/weights_database_model.ts @@ -0,0 +1,11 @@ +import { IsMongoId, IsString } from "class-validator"; +import { IWeightModel } from "./weights_validation_model"; + +export class WeightValidationModel implements IWeightModel { + @IsString() + public name: string; + @IsMongoId() + public datasetId: string; + @IsMongoId() + public project: string; +} diff --git a/server/src/features/weights/models/weights_validation_model.ts b/server/src/features/weights/models/weights_validation_model.ts new file mode 100644 index 0000000..6293041 --- /dev/null +++ b/server/src/features/weights/models/weights_validation_model.ts @@ -0,0 +1,49 @@ +import { Schema, model } from "mongoose"; +import { IProjectModel, projectSchema } from "../../_projects/models/project_database_model"; +import { datasetSchema } from "../../datasets/models/dataset_database_model"; +import { IDatasetModel } from "../../datasets/models/dataset_validation_model"; + +export interface IWeightModel { + name: string; + datasetId: string | IDatasetModel; + project: string | IProjectModel; +} +export const WeightSchema = new Schema({ + name: { + type: String, + }, + local_path: { + type: String, + }, + neuralNetworkName: { + type: String, + }, + processStatus: { + type: String, + default: "none", + }, + // the user selects + isFinished: { + type: Boolean, + default: false, + }, + datasetId: { + type: Schema.Types.ObjectId, + ref: datasetSchema, + autopopulate: true, + require: true, + }, + project: { + type: Schema.Types.ObjectId, + ref: projectSchema, + autopopulate: true, + require: true, + }, + processLogs: { + type: String, + }, +}).plugin(require("mongoose-autopopulate")); + +export const weightSchema = "Weight"; + +export const WeightDBModel = model(weightSchema, WeightSchema); diff --git a/server/src/features/weights/weights_presentation.ts b/server/src/features/weights/weights_presentation.ts new file mode 100644 index 0000000..8ee308c --- /dev/null +++ b/server/src/features/weights/weights_presentation.ts @@ -0,0 +1,19 @@ +import { CrudController } from "../../core/controllers/crud_controller"; +import { ExecWeightProcessScenario } from "./domain/exec_weights_process_scenario"; +import { WeightValidationModel } from "./models/weights_database_model"; +import { WeightDBModel } from "./models/weights_validation_model"; + +export class WeightsPresentation extends CrudController { + constructor() { + super({ + url: "weights", + validationModel: WeightValidationModel, + databaseModel: WeightDBModel, + }); + this.subRoutes.push({ + method: "GET", + subUrl: "exec", + fn: new ExecWeightProcessScenario(), + }); + } +} diff --git a/ui/src/core/extensions/array.ts b/ui/src/core/extensions/array.ts index 8454e31..6bfd983 100644 --- a/ui/src/core/extensions/array.ts +++ b/ui/src/core/extensions/array.ts @@ -54,7 +54,7 @@ export const ArrayExtensions = () => { if ([].repeat === undefined) { // eslint-disable-next-line no-extend-native Array.prototype.repeat = function (quantity) { - return Array(quantity).fill(this[0]); + return Array(quantity).fill(this).flat(1); }; } }; diff --git a/ui/src/core/model/style.ts b/ui/src/core/model/style.ts new file mode 100644 index 0000000..acfab5e --- /dev/null +++ b/ui/src/core/model/style.ts @@ -0,0 +1,3 @@ +export interface IStyle{ + style?: React.CSSProperties; +} diff --git a/ui/src/core/ui/button/button.tsx b/ui/src/core/ui/button/button.tsx index 3db5865..bdb73c3 100644 --- a/ui/src/core/ui/button/button.tsx +++ b/ui/src/core/ui/button/button.tsx @@ -1,12 +1,12 @@ import * as React from "react"; import { CoreText, CoreTextType } from "../text/text"; +import { IStyle } from "../../model/style"; -export interface IButtonProps { +export interface IButtonProps extends IStyle { block?: boolean; filled?: boolean; text?: string; onClick?: any; - style?: React.CSSProperties; } export function CoreButton(props: IButtonProps) { diff --git a/ui/src/core/ui/icons/icons.tsx b/ui/src/core/ui/icons/icons.tsx index 9952268..c4a61ef 100644 --- a/ui/src/core/ui/icons/icons.tsx +++ b/ui/src/core/ui/icons/icons.tsx @@ -1,9 +1,9 @@ import * as React from "react"; import { Result } from "../../helper/result"; +import { IStyle } from "../../model/style"; -export interface IIconsProps { +export interface IIconsProps extends IStyle { type: string; - style?: React.CSSProperties; onClick?: Function; } diff --git a/ui/src/core/ui/input/input.tsx b/ui/src/core/ui/input/input.tsx index 2aa86de..795f58a 100644 --- a/ui/src/core/ui/input/input.tsx +++ b/ui/src/core/ui/input/input.tsx @@ -1,11 +1,11 @@ import * as React from "react"; import { CoreText, CoreTextType } from "../text/text"; +import { IStyle } from "../../model/style"; -interface IInputProps { +interface IInputProps extends IStyle { label: string; value?: string; onChange?: (value: string) => void; - style?: React.CSSProperties; validation?: (value: string) => boolean; error?: string; } diff --git a/ui/src/core/ui/link/link.tsx b/ui/src/core/ui/link/link.tsx index 05e3bf3..8efafe7 100644 --- a/ui/src/core/ui/link/link.tsx +++ b/ui/src/core/ui/link/link.tsx @@ -1,13 +1,13 @@ import * as React from "react"; import { Typography } from "antd"; import { useNavigate } from "react-router-dom"; +import { IStyle } from "../../model/style"; const { Link } = Typography; -export interface ILinkTypography { +export interface ILinkTypography extends IStyle { path: string; text: string; - style?: React.CSSProperties; } export const LinkTypography: React.FunctionComponent = ( diff --git a/ui/src/core/ui/select/select.tsx b/ui/src/core/ui/select/select.tsx index b4cf558..cc4e4e9 100644 --- a/ui/src/core/ui/select/select.tsx +++ b/ui/src/core/ui/select/select.tsx @@ -1,12 +1,12 @@ import React from "react"; import { CoreText, CoreTextType } from "../text/text"; +import { IStyle } from "../../model/style"; -interface ISelectCoreProps { +interface ISelectCoreProps extends IStyle { items: string[]; value: string; label: string; onChange: (value: string) => void; - style?: React.CSSProperties; } export const SelectCore = (props: ISelectCoreProps) => { const ref = React.useRef(null); diff --git a/ui/src/features/dataset/card_dataset.tsx b/ui/src/features/dataset/card_dataset.tsx index 3b54c31..4dbcf98 100644 --- a/ui/src/features/dataset/card_dataset.tsx +++ b/ui/src/features/dataset/card_dataset.tsx @@ -133,7 +133,6 @@ export const CardDataSet = (props: ICardDataSetProps) => { />):null} {props.processStatus === ProcessStatus.RUN ? (<> { if (props.type.isEqual(CardDataSetType.COMPLETED) && props.onClickButton && props.id) { props.onClickButton(props.id); diff --git a/ui/src/features/dataset/p.json b/ui/src/features/dataset/p.json deleted file mode 100644 index 0519ecb..0000000 --- a/ui/src/features/dataset/p.json +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/src/features/skils/skil_card.tsx b/ui/src/features/skils/skil_card.tsx new file mode 100644 index 0000000..b45611b --- /dev/null +++ b/ui/src/features/skils/skil_card.tsx @@ -0,0 +1,74 @@ +import { CoreButton } from "../../core/ui/button/button"; +import { Icon } from "../../core/ui/icons/icons"; +import { CoreText, CoreTextType } from "../../core/ui/text/text"; +import { ProcessStatus } from "../dataset/dataset_model"; + +export interface ISkillCardProps { + processStatus?: string; + name?: string; + emptyOnClick?: Function; + empty: boolean; +} + +export const SkillCard = (props: ISkillCardProps) => { + return ( +
+ {props.empty ? ( +
{ + if (props.empty && props.emptyOnClick) props.emptyOnClick(); + }} + style={{ display: "flex", justifyContent: "center", alignItems: "center" }} + > + +
+ ) : ( + <> + + {props.processStatus === ProcessStatus.END ? ( + { + // if (props.type.isEqual(CardDataSetType.COMPLETED) && props.onClickButton && props.id) { + // props.onClickButton(props.id); + // } + }} + text="Завершен" + /> + ) : null} + {props.processStatus === ProcessStatus.RUN ? ( + <> + { + // if (props.type.isEqual(CardDataSetType.COMPLETED) && props.onClickButton && props.id) { + // props.onClickButton(props.id); + }} + block={true} + text="Стоп" + /> + + ) : null} + {props.processStatus === ProcessStatus.ERROR ? ( + {}} + filled={true} + text="Ошибка" + /> + ) : null} + + )} +
+ ); +}; diff --git a/ui/src/features/skils/skills_repository.tsx b/ui/src/features/skils/skills_repository.tsx new file mode 100644 index 0000000..b4e063c --- /dev/null +++ b/ui/src/features/skils/skills_repository.tsx @@ -0,0 +1,32 @@ +import { Result } from "../../core/helper/result"; +import { HttpError, HttpMethod, HttpRepository } from "../../core/repository/http_repository"; +import { UUID } from "../all_projects/data/project_repository"; +import { IDatasetModel } from "../dataset/dataset_model"; +import { SkillModel } from "./skills_store"; +export interface IEducations { + _id: string; + name: string; + processStatus: string; + isFinished: boolean; + datasetId: any; + project: any; + __v: number; +} + +export class SkillsRepository extends HttpRepository { + getAllSkills = async () => { + return this._jsonRequest(HttpMethod.GET, "/weights"); + }; + deleteSkill = async (id: string) => { + return this._jsonRequest(HttpMethod.DELETE, `/weights?id=${id}`); + }; + addNewSkill = async (model: SkillModel) => { + return this._jsonRequest(HttpMethod.POST, "/weights", model); + }; + getDatasetsActiveProject = async (): Promise> => { + return this._jsonRequest(HttpMethod.GET, "/datasets"); + }; + getActiveProjectId(): Promise> { + return this._jsonRequest(HttpMethod.GET, "/projects/get/active/project/id"); + } +} diff --git a/ui/src/features/skils/skills_screen.tsx b/ui/src/features/skils/skills_screen.tsx index ba22d5e..1142878 100644 --- a/ui/src/features/skils/skills_screen.tsx +++ b/ui/src/features/skils/skills_screen.tsx @@ -1,20 +1,97 @@ +import React from "react"; import { MainPage } from "../../core/ui/pages/main_page"; +import { CoreText, CoreTextType } from "../../core/ui/text/text"; +import { DrawersSkill, SkillStore } from "./skills_store"; +import { observer } from "mobx-react-lite"; +import { SkillCard } from "./skil_card"; +import { Drawer } from "antd"; +import { CoreInput } from "../../core/ui/input/input"; +import { CoreButton } from "../../core/ui/button/button"; +import { CoreSwitch } from "../../core/ui/switch/switch"; + +interface IItem { + name: string; + isActive: boolean; +} + +const skills: IItem[] = [{ name: "ML", isActive: true }]; export const SkillPath = "/skills"; -export const SkillScreen = () => { +export const SkillScreen = observer(() => { + const [store] = React.useState(() => new SkillStore()); + + React.useEffect(() => { + store.init(); + }, [store]); return ( <> + {skills.map((el) => ( + + ))} + + } bodyChildren={ - <> -
- -
- +
+ {store.skils?.map((el) => ( + + ))} + store.edtDrawer(DrawersSkill.NEW_SKILL, true)} /> +
} /> + store.edtDrawer(DrawersSkill.NEW_SKILL, false)} + open={store.drawers.find((el) => el.name === DrawersSkill.NEW_SKILL)?.status} + > +
+ store.changeSkillName(e)} /> +
+ {store.datasets?.map((el) => ( +
+ {el.name} + store.selectDataset(el._id)} + /> +
+ ))} +
+ +
+ store.saveSkill()} /> +
+ store.edtDrawer(DrawersSkill.NEW_SKILL, false)} /> +
+
+ ); -}; +}); diff --git a/ui/src/features/skils/skills_store.tsx b/ui/src/features/skils/skills_store.tsx index cfe354a..83c99f8 100644 --- a/ui/src/features/skils/skills_store.tsx +++ b/ui/src/features/skils/skills_store.tsx @@ -1,3 +1,93 @@ -export class SkillStore{ - -} \ No newline at end of file +import { NavigateFunction } from "react-router-dom"; +import { HttpError } from "../../core/repository/http_repository"; +import { UiErrorState } from "../../core/store/base_store"; +import makeAutoObservable from "mobx-store-inheritance"; +import { IEducations as ISkils, SkillsRepository as SkillsHttpRepository } from "./skills_repository"; +import { Drawer } from "../dataset/dataset_store"; +import { IDatasetModel } from "../dataset/dataset_model"; +import { Result } from "../../core/helper/result"; +import { message } from "antd"; +import { UUID } from "../all_projects/data/project_repository"; + +export enum DrawersSkill { + NEW_SKILL = "Новый навык", +} + +export class SkillModel { + constructor(public name: string, public datasetId: string, public project: string) { + makeAutoObservable(this); + } + static empty() { + return new SkillModel("", "", ""); + } + valid(): Result { + if (this.name.isEmpty()) { + return Result.error("name is empty"); + } + if (this.datasetId.isEmpty()) { + return Result.error("datasetId is empty"); + } + if (this.project.isEmpty()) { + return Result.error("project is empty"); + } + return Result.ok(this); + } +} +export class SkillStore extends UiErrorState { + drawers: Drawer[]; + skillsHttpRepository: SkillsHttpRepository; + skils?: ISkils[]; + datasets?: IDatasetModel[]; + activeProjectId?: UUID; + skill: SkillModel; + titleDrawer: string = DrawersSkill.NEW_SKILL; + + constructor() { + super(); + this.drawers = Object.entries(DrawersSkill).map((k, v) => { + return { + name: k.at(1) ?? "", + status: false, + }; + }); + this.skill = SkillModel.empty(); + this.skillsHttpRepository = new SkillsHttpRepository(); + makeAutoObservable(this); + } + errorHandingStrategy: (error: HttpError) => {}; + init = async (navigate?: NavigateFunction | undefined) => { + await this.mapOk("skils", this.skillsHttpRepository.getAllSkills()); + await this.mapOk("datasets", this.skillsHttpRepository.getDatasetsActiveProject()); + await this.mapOk("activeProjectId", this.skillsHttpRepository.getActiveProjectId()); + }; + changeSkillName(name: string): void { + this.skill.name = name; + } + saveSkill() { + console.log(this.activeProjectId); + this.skill.project = this.activeProjectId?.id ?? ""; + this.skill.valid().fold( + async (model) => { + (await this.skillsHttpRepository.addNewSkill(model)).fold( + (s) => { + message.success("Новый "); + this.skill = SkillModel.empty(); + }, + (e) => message.error(e.message) + ); + }, + async (e) => message.error(e) + ); + } + selectDataset(id: string): void { + this.skill.datasetId = id; + } + edtDrawer(drawerName: DrawersSkill, status: boolean): void { + this.drawers = this.drawers.map((el) => { + if (el.name === drawerName) { + el.status = status; + } + return el; + }); + } +} diff --git a/web_p/education.py b/web_p/education.py new file mode 100644 index 0000000..86363f7 --- /dev/null +++ b/web_p/education.py @@ -0,0 +1,28 @@ +import shutil +import argparse +import os.path +from pathlib import Path + +parser = argparse.ArgumentParser() +parser.add_argument("--path") +parser.add_argument("--name") + +args = parser.parse_args() + + +def copy_and_move_folder(src, dst, folder): + try: + if os.path.exists(dst + "/education/") is False: + Path(dst + "/education/").mkdir(parents=True, exist_ok=True) + if os.path.exists(dst + "/education/" + folder + "/"): + shutil.rmtree(dst + "/education/" + folder + "/") + + shutil.copytree(src, dst + "/education/" + folder + "/") + + except shutil.Error as e: + print(f"Error: {e}") + + +source_folder = os.path.dirname(os.path.abspath(__file__)) + "/education" + +copy_and_move_folder(source_folder, args.path, args.name) diff --git a/web_p/education/metadata.json b/web_p/education/metadata.json new file mode 100644 index 0000000..817e74e --- /dev/null +++ b/web_p/education/metadata.json @@ -0,0 +1,3 @@ +{ + "numberOfEpochs": 1 +} diff --git a/web_p/education/yolov8n.pt b/web_p/education/yolov8n.pt new file mode 100644 index 0000000..5587612 Binary files /dev/null and b/web_p/education/yolov8n.pt differ