├── README.md ├── compute_host ├── Dockerfile ├── example_model_blip │ ├── model_invoke.py │ └── requirements.txt ├── example_model_clip │ └── model_invoke.py ├── example_model_falcon7b │ ├── README.md │ ├── config.json │ ├── configuration_RW.py │ ├── flask_app.py │ ├── generation_config.json │ ├── model_invoke.py │ ├── modelling_RW.py │ └── requirements.txt ├── example_model_falcon7b_instruct │ ├── flask_app.py │ ├── model_invoke.py │ └── requirements.txt ├── example_model_sd │ ├── flask_app.py │ ├── model_invoke.py │ └── requirements.txt ├── fake_inference_app.py ├── flask_app.py.template ├── host_agent.py ├── model-stable-diffusion-xl │ ├── flask_app.py │ ├── model_invoke.py │ ├── requirements.txt │ └── test.py ├── requirements.txt ├── utils.py └── worker_agent.py └── server ├── backend ├── Dockerfile ├── app │ ├── __init__.py │ ├── auto_scaler.py │ ├── decorators.py │ ├── invoke_utils.py │ ├── job_matcher.py │ ├── load_jobs.py │ ├── models │ │ ├── __init__.py │ │ ├── api_key.py │ │ ├── inference_job.py │ │ ├── invocation.py │ │ ├── model.py │ │ ├── user.py │ │ └── worker.py │ ├── routes │ │ ├── __init__.py │ │ ├── api_key_routes.py │ │ ├── inference_job_routes.py │ │ ├── invocation_routes.py │ │ ├── login_routes.py │ │ ├── model_routes.py │ │ ├── user_routes.py │ │ ├── worker_routes.py │ │ └── worker_socketio_routes.py │ ├── templates │ │ └── email_verify.html │ └── utils.py ├── config.py ├── create_seed_data.py ├── pytest.ini ├── requirements.txt ├── run.py └── tests │ ├── __init__.py │ ├── integration_test.py │ ├── test_api_key_routes.py │ ├── test_inference_job_routes.py │ ├── test_job_matcher.py │ ├── test_model_routes.py │ ├── test_user_routes.py │ └── test_worker_routes.py ├── cert.pem ├── datadog ├── Dockerfile └── conf.d │ ├── backend.yaml │ ├── frontend.yaml │ ├── nginx.yaml │ └── redisdb.yaml ├── docker-compose-staging.yml ├── docker-compose.yml ├── docker-compose_frontend_dev.yml ├── frontend ├── Dockerfile ├── README.md ├── env_template ├── mock │ └── userAPI.ts ├── nginx.conf ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── access.ts │ ├── app.tsx │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── Guide │ │ │ ├── Guide.less │ │ │ ├── Guide.tsx │ │ │ └── index.ts │ │ └── UserAction │ │ │ ├── index.less │ │ │ └── index.tsx │ ├── constants │ │ └── index.ts │ ├── global.css │ ├── hooks │ │ ├── index.ts │ │ └── useColumnsState.ts │ ├── models │ │ └── global.ts │ ├── pages │ │ ├── Access │ │ │ └── index.tsx │ │ ├── Home │ │ │ ├── components │ │ │ │ └── Overview │ │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── InferenceJobs │ │ │ ├── components │ │ │ │ ├── CreateInferenceJobModal │ │ │ │ │ └── index.tsx │ │ │ │ └── GetInferenceJobModal │ │ │ │ │ ├── index.less │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── services.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── services.ts │ │ ├── Invocations │ │ │ ├── components │ │ │ │ └── InvocationDetailModal │ │ │ │ │ ├── index.less │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── services.ts │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── Login │ │ │ ├── assets │ │ │ │ └── logo.png │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── services.ts │ │ ├── ModelsPage │ │ │ ├── components │ │ │ │ ├── CreateModelModal │ │ │ │ │ └── index.tsx │ │ │ │ └── ModelDetailModal │ │ │ │ │ ├── index.less │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── services.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── services.ts │ │ ├── Register │ │ │ ├── assets │ │ │ │ └── logo.png │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── services.ts │ │ ├── TestInvocation │ │ │ ├── index.tsx │ │ │ └── services.ts │ │ └── Workers │ │ │ ├── index.tsx │ │ │ └── services.ts │ ├── services │ │ ├── global.ts │ │ └── invoke.ts │ └── utils │ │ ├── format.ts │ │ └── http │ │ ├── index.js │ │ └── src │ │ ├── Request.js │ │ ├── index.js │ │ ├── interceptors.js │ │ └── utils │ │ └── applyFn.js ├── tailwind.config.js ├── tailwind.css ├── tsconfig.json └── typings.d.ts ├── landingPage ├── Dockerfile ├── README.md ├── app.js ├── bootstrap.js ├── config.ts ├── f.yml ├── package.json ├── pm2.config.js ├── src │ ├── config │ │ └── config.default.ts │ ├── configuration.ts │ ├── controller │ │ ├── api.ts │ │ └── index.ts │ ├── interface │ │ ├── detail.ts │ │ └── index.ts │ ├── mock │ │ ├── detail.ts │ │ └── index.ts │ └── service │ │ ├── detail.ts │ │ └── index.ts ├── tailwind.config.js ├── tsconfig.build.json ├── tsconfig.json ├── typings │ └── data │ │ ├── detail-index.d.ts │ │ ├── foo.d.ts │ │ ├── index.d.ts │ │ └── page-index.d.ts └── web │ ├── @types │ ├── global.d.ts │ └── typings.d.ts │ ├── common.less │ ├── components │ ├── brief │ │ ├── index.module.less │ │ └── index.tsx │ ├── layout │ │ ├── App.tsx │ │ ├── assets │ │ │ └── favicon.ico │ │ ├── fetch.ts │ │ ├── global.css │ │ └── index.tsx │ ├── player │ │ ├── index.module.less │ │ └── index.tsx │ ├── recommend │ │ ├── index.module.less │ │ └── index.tsx │ ├── rectangle │ │ ├── index.module.less │ │ └── index.tsx │ └── search │ │ ├── index.module.less │ │ └── index.tsx │ ├── images │ └── icon.png │ ├── pages │ ├── detail │ │ ├── fetch.ts │ │ └── render$id.tsx │ └── index │ │ ├── components │ │ ├── CoreSection │ │ │ └── index.tsx │ │ ├── DemoSection │ │ │ ├── 2_1.gif │ │ │ ├── 2_2.gif │ │ │ └── index.tsx │ │ ├── EnvironmentSection │ │ │ ├── icon.jpg │ │ │ └── index.tsx │ │ ├── ExplainSection │ │ │ ├── icon.png │ │ │ └── index.tsx │ │ ├── Footer │ │ │ ├── index.tsx │ │ │ └── logo.png │ │ ├── Header │ │ │ ├── index.tsx │ │ │ ├── logo.png │ │ │ └── logo.png.zip │ │ ├── LandingSection │ │ │ └── index.tsx │ │ ├── ProviderSection │ │ │ └── index.tsx │ │ └── SDKSection │ │ │ ├── TextItem │ │ │ └── index.tsx │ │ │ ├── code.png │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── fetch.ts │ │ └── render.tsx │ └── store │ └── index.ts └── nginx.conf /README.md: -------------------------------------------------------------------------------- 1 | # ByteNovaAI 2 | 3 | ## Table of Contents 4 | 5 | * [Official Website](#Official-Website) 6 | * [Quick Start](#Quick-Start) 7 | * [Run Tests](#Run-Tests) 8 | * [SocketIO](#SocketIO) 9 | * [Scheduled Tasks](#Scheduled-Tasks) 10 | 11 | ## Official Website 12 | [clustro.ai](https://www.clustro.ai/) 13 | 14 | [console.clustro.ai](https://console.clustro.ai/) 15 | 16 | [user docs](https://docs.clustro.ai/) 17 | 18 | 19 | ## Quick Start 20 | 21 | ### compute_host 22 | compute_host is a process based on SocketIO communication. By starting the host_agent, the machine will be registered as a real worker. Subsequent jobs can be assigned and executed for this worker. 23 | 24 | #### Local Run 25 | Install dependencies 26 | ```bash 27 | pip install -r requirements.txt 28 | export CLUSTROAI_API_KEY=Your APi KEY 29 | ``` 30 | Run host_agent, it will create a temporary worker and communicate with the server. 31 | 32 | There are three optional arguments: 33 | 1. --server_url default http://api.clustro.ai:5000 34 | 2. --local_service_port default 8000 35 | 3. --gpu_limit default 999 36 | ```python 37 | python host_agent.py 38 | ``` 39 | 40 | #### Start with Docker 41 | ```bash 42 | docker build -t host_agent:v1 . 43 | docker run -d -e CLUSTROAI_API_KEY=Your APi KEY host_agent:v1 44 | ``` 45 | 46 | ### server 47 | Under server, there are backend, frontend, and landingPage. 48 | 49 | * backend is the project backend, primarily designed with technologies such as flask, postgres, and redis. 50 | * frontend is the frontend project primarily designed with technologies like react, umijs/max, and ant-design. 51 | * landingPage is the old frontend project, which will be phased out over time and can be ignored. 52 | #### backend 53 | ##### Local Start 54 | You need to have a postgres database and redis. It's recommended to install locally using Docker. 55 | ```bash 56 | docker pull postgres 57 | docker run -d --name=postgres -e POSTGRES_PASSWORD=your password -p 5432:5432 postgres 58 | docker pull redis 59 | docker run -d --name=redis -p 6379:6379 redis 60 | 61 | export DB_USER=postgres 62 | export DB_PASSWORD=your password 63 | export DB_HOST=localhost 64 | python run.py 65 | ``` 66 | 67 | ##### Start with Docker 68 | ```bash 69 | docker build -t backend:v1 . 70 | docker run -d -e DB_USER=postgres,DB_PASSWORD=your password,DB_HOST=localhost backend:v1 71 | ``` 72 | 73 | #### frontend 74 | Configure environment variables: 75 | ```bash 76 | cp env_template .env 77 | Modify REACT_APP_BACKEND_SERVER_URL to your backend's starting URL. 78 | ``` 79 | ##### Local Start 80 | ```bash 81 | npm install 82 | npm run start 83 | ``` 84 | ##### Start with Docker 85 | ```bash 86 | docker-compose up -d frontend 87 | ``` 88 | Visit URL http://localhost:3000 to view the page. 89 | 90 | ## Run Tests 91 | ```bash 92 | # Run all test cases 93 | DB_USER=postgres DB_PASSWORD=your password pytest 94 | # Run a single test case 95 | DB_USER=postgres DB_PASSWORD=your password pytest tests/test_worker_routes.py::test_update_worker 96 | ``` 97 | 98 | ## SocketIO 99 | When the backend starts, it launches a SocketIO server. When worker_agent starts, it connects to the backend's SocketIO and creates a namespace for communication. This facilitates the allocation of the inference job to this worker. The specific process is as follows: 100 | 1. worker_agent connects to the SocketIO server and initiates communication. 101 | 2. The SocketIO server sends a request_worker_id request. 102 | 3. worker_agent sends a provide_worker_id request. 103 | 4. The SocketIO server sends a worker_session_established request. 104 | 5. worker_agent initiates a request to start communication. 105 | 6. The SocketIO server sends prepare_model. At this point, the worker_agent begins to clone code from the git repository and starts the Flask app. 106 | 7. The inference job is assigned to worker_agent. 107 | 8. worker_agent begins execute_invocation and sends the results back to the SocketIO server. 108 | 109 | ## Scheduled Tasks 110 | 1. job_matching automatically assigns inference jobs to available idle workers. 111 | 2. The auto_scaler periodically scans the inference job, ensuring that workers that have completed their jobs are in an idle state. 112 | -------------------------------------------------------------------------------- /compute_host/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the specified NVIDIA CUDA base image 2 | FROM nvidia/cuda:11.6.2-runtime-ubuntu20.04 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Copy the specified files to the container 8 | COPY host_agent.py . 9 | COPY fake_inference_app.py . 10 | COPY requirements.txt . 11 | COPY utils.py . 12 | COPY flask_app.py.template . 13 | COPY worker_agent.py . 14 | 15 | # Install python3, pip, and git 16 | RUN apt update && apt install -y \ 17 | python3 \ 18 | python3-pip \ 19 | git \ 20 | lsof 21 | 22 | RUN export DEBIAN_FRONTEND=noninteractive && apt-get update -q && apt-get install -yq ffmpeg libsm6 libxext6 23 | 24 | # Install the Python requirements 25 | RUN pip3 install -r requirements.txt 26 | 27 | CMD ["python3", "host_agent.py"] 28 | -------------------------------------------------------------------------------- /compute_host/example_model_blip/model_invoke.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from PIL import Image 3 | from transformers import BlipProcessor, BlipForConditionalGeneration 4 | import json 5 | 6 | processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-large") 7 | model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-large").to("cuda") 8 | 9 | def invoke(input_text): 10 | try: 11 | input_json = json.loads(input_text) 12 | text = input_json['text'] 13 | image_url = input_json['image_url'] 14 | except: 15 | text = "" 16 | image_url = input_text 17 | raw_image = Image.open(requests.get(image_url, stream=True).raw).convert('RGB') 18 | if text: 19 | inputs = processor(raw_image, text, return_tensors="pt").to("cuda") 20 | else: 21 | inputs = processor(raw_image, return_tensors="pt").to("cuda") 22 | out = model.generate(**inputs) 23 | return processor.decode(out[0], skip_special_tokens=True) 24 | -------------------------------------------------------------------------------- /compute_host/example_model_blip/requirements.txt: -------------------------------------------------------------------------------- 1 | transformers 2 | -------------------------------------------------------------------------------- /compute_host/example_model_clip/model_invoke.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import requests 3 | import json 4 | 5 | from transformers import CLIPProcessor, CLIPModel 6 | 7 | model = CLIPModel.from_pretrained("openai/clip-vit-large-patch14") 8 | processor = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14") 9 | 10 | def invoke(input_text): 11 | input_json = json.loads(input_text) 12 | image_url = input_json['image_url'] 13 | image = Image.open(requests.get(image_url, stream=True).raw) 14 | text_arr = input_json['text'].split(",") 15 | inputs = processor(text=text_arr, images=image, return_tensors="pt", padding=True) 16 | outputs = model(**inputs) 17 | logits_per_image = outputs.logits_per_image # this is the image-text similarity score 18 | probs = logits_per_image.softmax(dim=1) # we can take the softmax to get the label probabilities 19 | return [text_arr[i]+": "+str(probs.tolist()[0][i]) for i in range(0,len(text_arr))] 20 | -------------------------------------------------------------------------------- /compute_host/example_model_falcon7b/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "alibi": false, 3 | "apply_residual_connection_post_layernorm": false, 4 | "architectures": [ 5 | "RWForCausalLM" 6 | ], 7 | "attention_dropout": 0.0, 8 | "auto_map": { 9 | "AutoConfig": "configuration_RW.RWConfig", 10 | "AutoModel": "modelling_RW.RWModel", 11 | "AutoModelForSequenceClassification": "modelling_RW.RWForSequenceClassification", 12 | "AutoModelForTokenClassification": "modelling_RW.RWForTokenClassification", 13 | "AutoModelForQuestionAnswering": "modelling_RW.RWForQuestionAnswering", 14 | "AutoModelForCausalLM": "modelling_RW.RWForCausalLM" 15 | }, 16 | "bias": false, 17 | "bos_token_id": 11, 18 | "eos_token_id": 11, 19 | "hidden_dropout": 0.0, 20 | "hidden_size": 4544, 21 | "initializer_range": 0.02, 22 | "layer_norm_epsilon": 1e-05, 23 | "model_type": "RefinedWebModel", 24 | "multi_query": true, 25 | "n_head": 71, 26 | "n_layer": 32, 27 | "parallel_attn": true, 28 | "torch_dtype": "bfloat16", 29 | "transformers_version": "4.27.4", 30 | "use_cache": true, 31 | "vocab_size": 65024 32 | } 33 | -------------------------------------------------------------------------------- /compute_host/example_model_falcon7b/configuration_RW.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Copyright 2022 the Big Science Workshop and HuggingFace Inc. team. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | """ Bloom configuration""" 16 | from transformers.configuration_utils import PretrainedConfig 17 | from transformers.utils import logging 18 | 19 | 20 | logger = logging.get_logger(__name__) 21 | 22 | 23 | class RWConfig(PretrainedConfig): 24 | model_type = "RefinedWebModel" 25 | keys_to_ignore_at_inference = ["past_key_values"] 26 | attribute_map = { 27 | "num_hidden_layers": "n_layer", 28 | "num_attention_heads": "n_head", 29 | } 30 | 31 | def __init__( 32 | self, 33 | vocab_size=250880, 34 | hidden_size=64, 35 | n_layer=2, 36 | n_head=8, 37 | layer_norm_epsilon=1e-5, 38 | initializer_range=0.02, 39 | use_cache=True, 40 | bos_token_id=1, 41 | eos_token_id=2, 42 | apply_residual_connection_post_layernorm=False, 43 | hidden_dropout=0.0, 44 | attention_dropout=0.0, 45 | multi_query=False, 46 | alibi=False, 47 | bias=False, 48 | parallel_attn=False, 49 | **kwargs, 50 | ): 51 | self.vocab_size = vocab_size 52 | # Backward compatibility with n_embed kwarg 53 | n_embed = kwargs.pop("n_embed", None) 54 | self.hidden_size = hidden_size if n_embed is None else n_embed 55 | self.n_layer = n_layer 56 | self.n_head = n_head 57 | self.layer_norm_epsilon = layer_norm_epsilon 58 | self.initializer_range = initializer_range 59 | self.use_cache = use_cache 60 | self.apply_residual_connection_post_layernorm = apply_residual_connection_post_layernorm 61 | self.hidden_dropout = hidden_dropout 62 | self.attention_dropout = attention_dropout 63 | 64 | self.bos_token_id = bos_token_id 65 | self.eos_token_id = eos_token_id 66 | self.multi_query = multi_query 67 | self.alibi = alibi 68 | self.bias = bias 69 | self.parallel_attn = parallel_attn 70 | 71 | super().__init__(bos_token_id=bos_token_id, eos_token_id=eos_token_id, **kwargs) 72 | 73 | @property 74 | def head_dim(self): 75 | return self.hidden_size // self.n_head 76 | 77 | @property 78 | def rotary(self): 79 | return not self.alibi 80 | -------------------------------------------------------------------------------- /compute_host/example_model_falcon7b/flask_app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from model_invoke import invoke 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route('/invoke', methods=['POST']) 7 | def generate(): 8 | text = request.json['input'] 9 | result = invoke(text) 10 | return result 11 | 12 | if __name__ == '__main__': 13 | app.run(host='0.0.0.0', port=8000) 14 | -------------------------------------------------------------------------------- /compute_host/example_model_falcon7b/generation_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from_model_config": true, 3 | "bos_token_id": 1, 4 | "eos_token_id": 2, 5 | "transformers_version": "4.27.4" 6 | } 7 | -------------------------------------------------------------------------------- /compute_host/example_model_falcon7b/model_invoke.py: -------------------------------------------------------------------------------- 1 | from transformers import AutoTokenizer, AutoModelForCausalLM 2 | import transformers 3 | import torch 4 | 5 | model = "tiiuae/falcon-7b" 6 | 7 | tokenizer = AutoTokenizer.from_pretrained(model) 8 | pipeline = transformers.pipeline( 9 | "text-generation", 10 | model=model, 11 | tokenizer=tokenizer, 12 | torch_dtype=torch.bfloat16, 13 | trust_remote_code=True, 14 | device_map="auto", 15 | ) 16 | 17 | def invoke(input_text): 18 | sequences = pipeline( 19 | input_text, 20 | max_length=50, 21 | do_sample=True, 22 | top_k=10, 23 | num_return_sequences=1, 24 | eos_token_id=tokenizer.eos_token_id, 25 | ) 26 | result = "" 27 | for seq in sequences: 28 | result += seq['generated_text'] 29 | return result 30 | -------------------------------------------------------------------------------- /compute_host/example_model_falcon7b/requirements.txt: -------------------------------------------------------------------------------- 1 | transformers 2 | torch 3 | einops 4 | accelerate 5 | flask 6 | -------------------------------------------------------------------------------- /compute_host/example_model_falcon7b_instruct/flask_app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from model_invoke import invoke 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route('/invoke', methods=['POST']) 7 | def generate(): 8 | text = request.json['input'] 9 | result = invoke(text) 10 | return result 11 | 12 | if __name__ == '__main__': 13 | app.run(host='0.0.0.0', port=8000) 14 | -------------------------------------------------------------------------------- /compute_host/example_model_falcon7b_instruct/model_invoke.py: -------------------------------------------------------------------------------- 1 | from transformers import AutoTokenizer, AutoModelForCausalLM 2 | import transformers 3 | import torch 4 | import json 5 | 6 | model = "tiiuae/falcon-7b-instruct" 7 | 8 | tokenizer = AutoTokenizer.from_pretrained(model) 9 | pipeline = transformers.pipeline( 10 | "text-generation", 11 | model=model, 12 | tokenizer=tokenizer, 13 | torch_dtype=torch.bfloat16, 14 | trust_remote_code=True, 15 | device_map="auto", 16 | ) 17 | 18 | def invoke(input_text): 19 | try: 20 | input_json = json.loads(input_text) 21 | except: 22 | sequences = pipeline( 23 | input_text, 24 | max_length=50, 25 | do_sample=True, 26 | top_k=10, 27 | num_return_sequences=1, 28 | eos_token_id=tokenizer.eos_token_id, 29 | ) 30 | result = "" 31 | for seq in sequences: 32 | result += seq['generated_text'] 33 | return result 34 | try: 35 | input_json = json.loads(input_text) 36 | if 'prompt' not in input_json: 37 | sequences = pipeline( 38 | input_text, 39 | max_length=50, 40 | do_sample=True, 41 | top_k=10, 42 | num_return_sequences=1, 43 | eos_token_id=tokenizer.eos_token_id, 44 | ) 45 | else: 46 | prompt = input_json['prompt'] 47 | max_length = int(input_json['max_length']) if 'max_length' in input_json else 50 48 | top_k = int(input_json['top_k']) if 'top_k' in input_json else 10 49 | do_sample = input_json['do_sample'].lower()=="true" if 'do_sample' in input_json else True 50 | num_return_sequences = int(input_json['num_return_sequences']) if 'num_return_sequences' in input_json else 1 51 | 52 | sequences = pipeline( 53 | prompt, 54 | max_length=max_length, 55 | do_sample=do_sample, 56 | top_k=top_k, 57 | num_return_sequences=num_return_sequences, 58 | eos_token_id=tokenizer.eos_token_id, 59 | ) 60 | result = "" 61 | for seq in sequences: 62 | result += seq['generated_text'] 63 | return result 64 | except Exception as e: 65 | result = "Error: " + str(e) 66 | return result 67 | -------------------------------------------------------------------------------- /compute_host/example_model_falcon7b_instruct/requirements.txt: -------------------------------------------------------------------------------- 1 | transformers 2 | torch 3 | einops 4 | accelerate 5 | flask 6 | -------------------------------------------------------------------------------- /compute_host/example_model_sd/flask_app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from model_invoke import invoke 3 | import requests, base64, json 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/invoke', methods=['POST']) 8 | def generate(): 9 | rj = request.json 10 | text = rj['input'] 11 | result = invoke(text) 12 | 13 | if 's3_upload_fields' not in rj: 14 | return result 15 | s3_upload_fields = json.loads(base64.b64decode(rj['s3_upload_fields'].encode()).decode()) 16 | response = requests.post(s3_upload_fields['url'], data=s3_upload_fields['fields'], files={'file': open('generated_image.png', 'rb')}) 17 | # Check if the upload was successful 18 | if response.status_code >= 300: 19 | return {'result': f'Upload failed with status code {response.status_code}, response: {response.text}'} 20 | else: 21 | return {'result': "https://cdn.clustro.ai/" + s3_upload_fields['fields']['key']} 22 | 23 | if __name__ == '__main__': 24 | app.run(host='0.0.0.0', port=8000) 25 | -------------------------------------------------------------------------------- /compute_host/example_model_sd/model_invoke.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler 3 | 4 | model_id = "stabilityai/stable-diffusion-2-1" 5 | 6 | pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16) 7 | pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) 8 | pipe = pipe.to("cuda") 9 | 10 | def invoke(input_text): 11 | image = pipe(input_text).images[0] 12 | image.save('generated_image.png') 13 | return "generated_image.png" 14 | -------------------------------------------------------------------------------- /compute_host/example_model_sd/requirements.txt: -------------------------------------------------------------------------------- 1 | diffusers 2 | transformers 3 | accelerate 4 | scipy 5 | safetensors 6 | -------------------------------------------------------------------------------- /compute_host/fake_inference_app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | import time 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route('/invoke', methods=['POST']) 7 | def generate(): 8 | request_json = request.json 9 | text = request_json['input'] 10 | time.sleep(1) 11 | if 's3_upload_fields' in request_json: 12 | result = f'https://cdn.clustro.ai/generated-data/test.png' 13 | else: 14 | result = f'{text} ... some more text generated by model.' 15 | return result 16 | 17 | if __name__ == '__main__': 18 | app.run(host='0.0.0.0', port=8000) 19 | -------------------------------------------------------------------------------- /compute_host/flask_app.py.template: -------------------------------------------------------------------------------- 1 | from model_repo.model_invoke import invoke # This will be replaced 2 | from flask import Flask, request 3 | import requests, base64, json 4 | import argparse 5 | 6 | app = Flask(__name__) 7 | 8 | @app.route('/invoke', methods=['POST']) 9 | def generate(): 10 | rj = request.json 11 | text = rj['input'] 12 | result = invoke(text) 13 | 14 | if 's3_upload_fields' not in rj: 15 | return result 16 | 17 | s3_upload_fields = json.loads(base64.b64decode(rj['s3_upload_fields'].encode()).decode()) 18 | response = requests.post(s3_upload_fields['url'], data=s3_upload_fields['fields'], files={'file': open('generated_image.png', 'rb')}) 19 | # Check if the upload was successful 20 | if response.status_code >= 300: 21 | return f'Upload failed with status code {response.status_code}, response: {response.text}' 22 | else: 23 | return "https://cdn.clustro.ai/" + s3_upload_fields['fields']['key'] 24 | 25 | @app.route('/ping', methods=['GET']) 26 | def ping(): 27 | return "OK", 200 28 | 29 | if __name__ == '__main__': 30 | parser = argparse.ArgumentParser(description='Flask app') 31 | parser.add_argument('--port', type=int, default=8000, help='Port to run the app on') 32 | args = parser.parse_args() 33 | app.run(host='0.0.0.0', port=args.port) 34 | -------------------------------------------------------------------------------- /compute_host/host_agent.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import utils 3 | import subprocess 4 | import sys 5 | import os 6 | 7 | parser = argparse.ArgumentParser(description='Python Socket.IO Client for Host agent') 8 | parser.add_argument('--server_url', type=str, default='http://api.clustro.ai:5000', 9 | help='backend server url') 10 | parser.add_argument('--local_service_port', type=str, default='8000', 11 | help='local server port') 12 | parser.add_argument('--gpu_limit', type=str, help='GPU limit override', default='999') 13 | args = parser.parse_args() 14 | SERVER_URL = args.server_url 15 | LOCAL_SERVICE_PORT = args.local_service_port 16 | gpu_limit_override = args.gpu_limit 17 | if "CLUSTROAI_API_KEY" not in os.environ: 18 | print("Error: CLUSTROAI_API_KEY must be set in environment variables.") 19 | sys.exit(1) 20 | 21 | def start_worker_process(worker_id, local_service_port): 22 | command = [ 23 | 'python3', 24 | 'worker_agent.py', 25 | f'--worker_id={worker_id}', 26 | f'--server_url={SERVER_URL}', 27 | f'--local_service_port={local_service_port}' 28 | ] 29 | process = subprocess.Popen(command, preexec_fn=os.setsid) 30 | try: 31 | process.wait() 32 | except KeyboardInterrupt: 33 | # On keyboard interrupt, terminate the subprocess 34 | process.terminate() 35 | process.wait() 36 | 37 | if "CLUSTROAI_WORKER_ID" in os.environ: 38 | start_worker_process(os.environ["CLUSTROAI_WORKER_ID"], LOCAL_SERVICE_PORT) 39 | else: 40 | # TODO: Create multiple workers with GPU limit 41 | print("CLUSTROAI_WORKER_ID not found. Creating a temporary worker...") 42 | worker_sys_info = utils.get_system_info() 43 | available_gpu = min(worker_sys_info['gpu_memory_gb'], int(gpu_limit_override)) 44 | worker_id = utils.create_temp_worker(api_key=os.environ["CLUSTROAI_API_KEY"], available_gpu=available_gpu, api_url= SERVER_URL) 45 | start_worker_process(worker_id, LOCAL_SERVICE_PORT) 46 | -------------------------------------------------------------------------------- /compute_host/model-stable-diffusion-xl/flask_app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from model_invoke import invoke 3 | import requests, base64, json 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/invoke', methods=['POST']) 8 | def generate(): 9 | rj = request.json 10 | text = rj['input'] 11 | result = invoke(text) 12 | 13 | if 's3_upload_fields' not in rj: 14 | return result 15 | s3_upload_fields = json.loads(base64.b64decode(rj['s3_upload_fields'].encode()).decode()) 16 | response = requests.post(s3_upload_fields['url'], data=s3_upload_fields['fields'], files={'file': open('generated_image.png', 'rb')}) 17 | # Check if the upload was successful 18 | if response.status_code >= 300: 19 | return f'Upload failed with status code {response.status_code}, response: {response.text}' 20 | else: 21 | return "https://cdn.clustro.ai/" + s3_upload_fields['fields']['key'] 22 | 23 | if __name__ == '__main__': 24 | app.run(host='0.0.0.0', port=8000) 25 | -------------------------------------------------------------------------------- /compute_host/model-stable-diffusion-xl/model_invoke.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from diffusers import DiffusionPipeline 3 | 4 | # load both base & refiner 5 | base = DiffusionPipeline.from_pretrained( 6 | "stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16, variant="fp16", use_safetensors=True 7 | ) 8 | # base.to("cuda") 9 | base.enable_model_cpu_offload() 10 | refiner = DiffusionPipeline.from_pretrained( 11 | "stabilityai/stable-diffusion-xl-refiner-1.0", 12 | text_encoder_2=base.text_encoder_2, 13 | vae=base.vae, 14 | torch_dtype=torch.float16, 15 | use_safetensors=True, 16 | variant="fp16", 17 | ) 18 | refiner.to("cuda") 19 | 20 | # Define how many steps and what % of steps to be run on each experts (80/20) here 21 | n_steps = 40 22 | high_noise_frac = 0.8 23 | 24 | def invoke(input_text): 25 | image = base( 26 | prompt=input_text, 27 | num_inference_steps=n_steps, 28 | denoising_end=high_noise_frac, 29 | output_type="latent", 30 | ).images 31 | image = refiner( 32 | prompt=input_text, 33 | num_inference_steps=n_steps, 34 | denoising_start=high_noise_frac, 35 | image=image, 36 | ).images[0] 37 | image.save('generated_image.png') 38 | # # Convert the image to bytes 39 | # img_byte_arr = io.BytesIO() 40 | # image.save(img_byte_arr, format='PNG') 41 | # img_byte_arr = img_byte_arr.getvalue() 42 | return "generated_image.png" 43 | -------------------------------------------------------------------------------- /compute_host/model-stable-diffusion-xl/requirements.txt: -------------------------------------------------------------------------------- 1 | invisible_watermark 2 | transformers 3 | accelerate 4 | safetensors -------------------------------------------------------------------------------- /compute_host/model-stable-diffusion-xl/test.py: -------------------------------------------------------------------------------- 1 | from diffusers import DiffusionPipeline 2 | import torch 3 | 4 | # load both base & refiner 5 | base = DiffusionPipeline.from_pretrained( 6 | "stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16, variant="fp16", use_safetensors=True 7 | ) 8 | base.to("cuda") 9 | refiner = DiffusionPipeline.from_pretrained( 10 | "stabilityai/stable-diffusion-xl-refiner-1.0", 11 | text_encoder_2=base.text_encoder_2, 12 | vae=base.vae, 13 | torch_dtype=torch.float16, 14 | use_safetensors=True, 15 | variant="fp16", 16 | ) 17 | refiner.to("cuda") 18 | 19 | # Define how many steps and what % of steps to be run on each experts (80/20) here 20 | n_steps = 40 21 | high_noise_frac = 0.8 22 | 23 | prompt = "Yosemite firefall at sunset" 24 | 25 | # run both experts 26 | image = base( 27 | prompt=prompt, 28 | num_inference_steps=n_steps, 29 | denoising_end=high_noise_frac, 30 | output_type="latent", 31 | ).images 32 | image = refiner( 33 | prompt=prompt, 34 | num_inference_steps=n_steps, 35 | denoising_start=high_noise_frac, 36 | image=image, 37 | ).images[0] 38 | 39 | image.save('test.png') -------------------------------------------------------------------------------- /compute_host/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | python-engineio==4.4.1 3 | python-socketio[client]==5.8.0 4 | argparse 5 | requests 6 | invisible_watermark 7 | transformers 8 | accelerate 9 | safetensors 10 | diffusers 11 | scipy 12 | torch 13 | einops 14 | omegaconf -------------------------------------------------------------------------------- /compute_host/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import requests 4 | import shutil 5 | import string 6 | import random 7 | 8 | MODEL_DIR = 'model_repo' 9 | APP_TEMPLATE_NAME = 'flask_app.py.template' 10 | 11 | def get_system_info(): 12 | # Get GPU information using nvidia-smi 13 | try: 14 | result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader,nounits'], stdout=subprocess.PIPE) 15 | gpu_info = result.stdout.decode('utf-8').strip().split('\n')[0] 16 | gpu_name, gpu_memory = gpu_info.split(', ') 17 | except Exception as e: 18 | gpu_name, gpu_memory = "N/A", "N/A" 19 | 20 | if gpu_memory == "N/A": 21 | gpu_memory_gb = 0 22 | else: 23 | gpu_memory_gb = int(int(gpu_memory)/1024) 24 | 25 | # Get free disk space 26 | st = os.statvfs('/') 27 | free_disk_gb = int(st.f_bavail * st.f_frsize / (1024**3)) 28 | 29 | # Get external IP using requests 30 | try: 31 | response = requests.get('http://ifconfig.me', timeout=10) 32 | response.raise_for_status() # Raises an exception for HTTP errors 33 | ip = response.text.strip() 34 | except Exception as e: 35 | ip = "N/A" 36 | 37 | return {"gpu_name": gpu_name, "gpu_memory_gb": gpu_memory_gb, "free_disk_gb": free_disk_gb, "ip": ip} 38 | 39 | def get_inference_job_id(worker_id, api_key): 40 | worker_info = get_worker(worker_id, api_key) 41 | return worker_info['working_on'] 42 | 43 | def _get_resource(endpoint, api_key): 44 | headers = { 45 | 'Content-Type': 'application/json', 46 | 'X-API-Key': api_key 47 | } 48 | url = f"https://console.clustro.ai{endpoint}" 49 | response = requests.get(url, headers=headers) 50 | # Check if the request was successful 51 | response.raise_for_status() 52 | return response.json() 53 | 54 | def get_worker(worker_id, api_key): 55 | return _get_resource(f"/api/workers/{worker_id}", api_key) 56 | 57 | def get_inference_job(job_id, api_key): 58 | return _get_resource(f"/api/inference_jobs/{job_id}", api_key) 59 | 60 | def get_model(model_id, api_key): 61 | return _get_resource(f"/api/models/{model_id}", api_key) 62 | 63 | def get_model_from_worker(worker_id, api_key): 64 | worker = get_worker(worker_id, api_key) 65 | if worker['working_on'] is None: 66 | return None 67 | job = get_inference_job(worker['working_on'], api_key) 68 | model = get_model(job['model_id'], api_key) 69 | return model 70 | 71 | def get_model_repo(url, sha, model_invoke_function): 72 | if not os.path.exists(MODEL_DIR): 73 | os.mkdir(MODEL_DIR) 74 | model_file_name = get_model_file_path(url, sha) 75 | if os.path.exists(model_file_name): 76 | print("Model repo already prepared") 77 | return model_file_name 78 | # Clone the repository from the provided URL directly into 'model_repo' 79 | subprocess.run(['git', 'clone', url, model_file_name], check=True) 80 | subprocess.run(['git', '-C', model_file_name, 'checkout', sha], check=True) 81 | # TODO; prepare virtual env for package dependencies 82 | package, function = model_invoke_function.split('/') 83 | package_name = package.split('.py')[0] 84 | # Read the contents of flask_app.py.template 85 | with open('flask_app.py.template', 'r') as file: 86 | lines = file.readlines() 87 | lines[0] = f"from {package_name} import {function} as invoke\n" 88 | # Write the modified content to a new file flask_app.py 89 | with open('flask_app.py', 'w') as file: 90 | file.writelines(lines) 91 | shutil.copy2('flask_app.py', model_file_name) 92 | return model_file_name 93 | 94 | def get_model_file_path(url, sha): 95 | model_file_name = MODEL_DIR + "/" + _get_repo_name_from_url(url) + "_" + sha 96 | return model_file_name 97 | 98 | def _get_repo_name_from_url(url): 99 | # Split the URL by the slashes 100 | parts = url.rstrip('/').split('/') 101 | # Return the last part, which is usually the repo name 102 | return parts[-1].replace('.git', '') 103 | 104 | def _generate_random_string(length=8): 105 | characters = string.ascii_lowercase + string.digits # includes lowercase letters and numbers 106 | return ''.join(random.choice(characters) for _ in range(length)) 107 | 108 | def create_temp_worker(api_key, name=_generate_random_string(), available_gpu=0, type='temp', job_assignment_type='auto', api_url = "https://api.clustro.ai/api"): 109 | headers = { 110 | 'Content-Type': 'application/json', 111 | 'X-API-Key': api_key 112 | } 113 | url = f"{api_url}/workers/" 114 | payload = { 115 | 'name': name, 116 | 'type': type, 117 | 'job_assignment_type': job_assignment_type, 118 | 'available_gpu': available_gpu 119 | } 120 | response = requests.post(url, headers=headers, json=payload) 121 | # Check if the request was successful 122 | response.raise_for_status() 123 | return response.json()["id"] -------------------------------------------------------------------------------- /server/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.9-slim-buster 2 | WORKDIR / 3 | COPY requirements.txt . 4 | RUN python3 -m ensurepip --upgrade 5 | RUN pip install -r requirements.txt 6 | COPY . . 7 | # CMD ["sh", "-c", "FLASK_ENV=production gunicorn -w 1 -b 0.0.0.0:5000 run:app --timeout 3600"] 8 | CMD ["sh", "-c", "FLASK_ENV=production python run.py"] 9 | -------------------------------------------------------------------------------- /server/backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | # File: app/__init__.py 2 | from flask import Flask 3 | from flask_sqlalchemy import SQLAlchemy 4 | import redis, logging, os 5 | from flask_socketio import SocketIO 6 | from flask_cors import CORS 7 | 8 | db = SQLAlchemy() 9 | 10 | def create_app(config_name): 11 | app = Flask(__name__) 12 | app.config.from_object(config_name) 13 | db.init_app(app) 14 | 15 | return app 16 | 17 | if os.getenv('FLASK_ENV') == 'production': 18 | app = create_app('config.ProductionConfig') 19 | else: 20 | app = create_app('config.DevelopmentConfig') 21 | 22 | CORS(app, origins='*') 23 | 24 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 25 | app.json.compact = False 26 | 27 | socketio = SocketIO(app) 28 | 29 | # Create a Redis connection 30 | app.r = redis.Redis(host=app.config['REDIS_HOST'], port=6379, db=0) 31 | 32 | from app.routes import inference_job_routes # import the inference job routes 33 | from app.routes import invocation_routes # import the invocation routes 34 | from app.routes import worker_routes # import the worker routes 35 | from app.routes import user_routes # import the user routes 36 | from app.routes import model_routes # import the model routes 37 | from app.routes import api_key_routes # import the api key routes 38 | from app.routes import login_routes # import the login routes 39 | from app.routes.worker_socketio_routes import * # import the worker socketio routes 40 | 41 | app.register_blueprint(inference_job_routes.bp, url_prefix='/inference_jobs') # register the inference job blueprint 42 | app.register_blueprint(invocation_routes.bp, url_prefix='/invocations') # register the invocations blueprint 43 | app.register_blueprint(worker_routes.bp, url_prefix='/workers') # register the workers blueprint 44 | app.register_blueprint(user_routes.bp, url_prefix='/users') # register the users blueprint 45 | app.register_blueprint(model_routes.bp, url_prefix='/models') # register the models blueprint 46 | app.register_blueprint(api_key_routes.bp, url_prefix='/api_keys') # register the api keys blueprint 47 | app.register_blueprint(login_routes.bp, url_prefix='/login') # register the login blueprint 48 | 49 | # Check if the log directory exists 50 | logdir = os.path.join(os.getcwd(), 'logs') 51 | if not os.path.exists(logdir): 52 | os.makedirs(logdir) 53 | 54 | # Make sure app.logger uses the correct logger 55 | def setup_logging(): 56 | if not app.debug: 57 | # In production mode, add log handler to sys.stderr. 58 | app.logger.addHandler(logging.StreamHandler()) 59 | app.logger.setLevel(logging.INFO) 60 | 61 | handler = logging.FileHandler(os.path.join(logdir, 'flask.log')) 62 | handler.setLevel(logging.INFO) 63 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 64 | handler.setFormatter(formatter) 65 | app.logger.addHandler(handler) 66 | -------------------------------------------------------------------------------- /server/backend/app/auto_scaler.py: -------------------------------------------------------------------------------- 1 | # File: app/auto_scaler.py 2 | from app.models.inference_job import InferenceJob 3 | from app.models.invocation import Invocation 4 | from app import app 5 | 6 | def auto_scale(dry_run=False): 7 | inference_jobs = InferenceJob.query.filter(InferenceJob.desired_workers > InferenceJob.min_workers, InferenceJob.enabled, InferenceJob.scaling_type == 'auto').all() 8 | scaled_jobs = [] 9 | for inference_job in inference_jobs: 10 | key = f'invocation_heartbeat_{inference_job.id}' 11 | waiting_invocation_count = Invocation.query.filter(Invocation.inference_job_id == inference_job.id, Invocation.status == 'ENQUEUED').count() 12 | if not app.r.get(key) and waiting_invocation_count == 0: 13 | scaled_jobs.append(inference_job) 14 | if not dry_run: 15 | inference_job.scale_down_a_worker() 16 | if app.debug: 17 | app.logger.info(f'{len(scaled_jobs)} Jobs that were scaled down: {scaled_jobs}') -------------------------------------------------------------------------------- /server/backend/app/decorators.py: -------------------------------------------------------------------------------- 1 | # File: app/decorators.py 2 | import functools 3 | 4 | from flask import request, g, jsonify, current_app 5 | from functools import wraps 6 | from app.models.api_key import ApiKey 7 | 8 | def api_key_required(f): 9 | @wraps(f) 10 | def decorated_function(*args, **kwargs): 11 | api_key = request.headers.get('X-API-Key') 12 | if not api_key: 13 | return jsonify({"error": "API key required"}), 403 14 | 15 | api_key_obj = ApiKey.query.filter_by(key=api_key).first() 16 | if not api_key_obj: 17 | return jsonify({"error": "Invalid API key"}), 403 18 | g.user = api_key_obj.user 19 | try: 20 | return f(*args, **kwargs) 21 | except Exception as e: 22 | current_app.logger.exception(str(e)) 23 | return jsonify({"error": str(e)}), 403 24 | 25 | return decorated_function 26 | 27 | # Use only when allowing API requests without AuthN 28 | def api_key_required_with_exception(allowed_ids=None): 29 | def decorator(f): 30 | @wraps(f) 31 | def decorated_function(*args, **kwargs): 32 | id = kwargs.get('id', None) 33 | if not allowed_ids or (id and id not in allowed_ids): 34 | api_key = request.headers.get('X-API-Key') 35 | if not api_key: 36 | return jsonify({"error": "API key required"}), 403 37 | 38 | api_key_obj = ApiKey.query.filter_by(key=api_key).first() 39 | if not api_key_obj: 40 | return jsonify({"error": "Invalid API key"}), 403 41 | g.user = api_key_obj.user 42 | try: 43 | return f(*args, **kwargs) 44 | except Exception as e: 45 | current_app.logger.exception(str(e)) 46 | return jsonify({"error": str(e)}), 403 47 | return decorated_function 48 | return decorator -------------------------------------------------------------------------------- /server/backend/app/invoke_utils.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from app.models.invocation import Invocation 3 | from app.models.worker import Worker 4 | 5 | 6 | def inference_job_heartbeat(inference_job): 7 | key = f'invocation_heartbeat_{inference_job.id}' 8 | current_app.r.set(key, "invocated", ex=60) 9 | 10 | 11 | def check_and_scale_inference_job_workers(inference_job): 12 | if not inference_job.scaling_type == 'auto' or inference_job.desired_workers == inference_job.max_workers: 13 | return 14 | detault_ttl = 60 15 | key = f'autoscaling_inference_job_{inference_job.id}' 16 | # Try to set the key with a TTL of 60 seconds (1 minute) if it doesn't already exist 17 | result = current_app.r.set(key, 'wip', ex=detault_ttl, nx=True) 18 | if result: 19 | if (inference_job.desired_workers == 0 and inference_job.max_workers > 0) or ( 20 | Invocation.query.filter_by(inference_job_id=inference_job.id, status='ENQUEUED').count() > 1): 21 | inference_job.desired_workers = 1 22 | inference_job.save() 23 | workers = Worker.get_workers_waiting_for_a_job(gpu_limit=inference_job.model_required_gpu) 24 | if workers: 25 | w = workers[0] 26 | w.working_on = inference_job.id 27 | w.save() 28 | else: 29 | # The key already exists, refresh its TTL to 60 seconds 30 | current_app.r.expire(key, detault_ttl) -------------------------------------------------------------------------------- /server/backend/app/job_matcher.py: -------------------------------------------------------------------------------- 1 | # File: app/job_matcher.py 2 | from app.models.inference_job import InferenceJob 3 | from app.models.worker import Worker 4 | from collections import deque 5 | from app import app 6 | 7 | def match_jobs(dry_run=False): 8 | job_queue = _get_job_queue() 9 | idle_worker_queue = _get_idle_workers() 10 | matched_jobs = [] 11 | 12 | unable_to_match = [] 13 | 14 | while job_queue: 15 | job = job_queue.pop(0) 16 | matched = False 17 | 18 | # Use a loop to go through the deque and try to find a matching worker for the job 19 | for _ in range(len(idle_worker_queue)): # This loop guarantees we check each worker once 20 | worker = idle_worker_queue.popleft() 21 | if worker.available_gpu >= job[1]: # job[1] contains model_required_gpu 22 | if not dry_run: 23 | worker.working_on = job[0] # job[0] contains job.id 24 | worker.save() # Save the worker's state 25 | matched_jobs.append([worker.id, job[0]]) # job[0] contains job.id, worker.id contains worker.id 26 | matched = True 27 | break 28 | else: 29 | idle_worker_queue.append(worker) # Re-add the worker to the right side of the deque 30 | 31 | if not matched: 32 | unable_to_match.append(job[0]) 33 | 34 | if not idle_worker_queue: 35 | # print("No more idle workers") 36 | break 37 | 38 | if app.debug: 39 | app.logger.info(f'{len(matched_jobs)} Jobs that were matched: {matched_jobs}') 40 | app.logger.info(f'{len(unable_to_match)} Jobs that could not be matched: {unable_to_match}') 41 | app.logger.info(f'{len(idle_worker_queue)} Remaining idle workers: {list(idle_worker_queue)}') 42 | return matched_jobs 43 | 44 | 45 | def _get_job_queue(): 46 | jobs = InferenceJob.get_jobs_to_match() 47 | queue = [] 48 | for job in jobs: 49 | N = job.desired_workers - job.get_activate_worker_count() 50 | if N > 0: 51 | queue.extend([[job.id, job.model_required_gpu] for _ in range(N)]) 52 | # Sorting the queue based on the model_required_gpu (index 1 of the sublist) in descending order 53 | queue = sorted(queue, key=lambda x: x[1], reverse=True) 54 | return queue 55 | 56 | def _get_idle_workers(): 57 | sorted_idle_workers = sorted(Worker.get_workers_waiting_for_a_job(), key=lambda worker: worker.available_gpu, reverse=True) 58 | # Convert the sorted list to a deque 59 | return deque(sorted_idle_workers) 60 | -------------------------------------------------------------------------------- /server/backend/app/load_jobs.py: -------------------------------------------------------------------------------- 1 | # File: app/load_jobs.py 2 | from app import app 3 | from app.models.inference_job import InferenceJob 4 | from app.models.invocation import Invocation 5 | from app.models.worker import Worker 6 | from app import db 7 | 8 | def load_invocations_to_redis(): 9 | with app.app_context(): 10 | app.r.flushall() 11 | jobs = InferenceJob.query.all() 12 | for job in jobs: 13 | job_id_str = str(job.id) 14 | invocations = Invocation.query.filter_by(job_id=job.id).filter(Invocation.status != 'COMPLETED').all() 15 | for invocation in invocations: 16 | invocation_id_str = str(invocation.id) 17 | app.r.lpush(f'job:{job_id_str}', invocation_id_str) 18 | 19 | def reset_worker_connection_status(): 20 | with app.app_context(): 21 | workers = Worker.query.all() 22 | for w in workers: 23 | w.connected = False 24 | if w.job_assignment_type == "auto": 25 | w.working_on = None 26 | db.session.add(w) 27 | db.session.commit() 28 | -------------------------------------------------------------------------------- /server/backend/app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/backend/app/models/__init__.py -------------------------------------------------------------------------------- /server/backend/app/models/api_key.py: -------------------------------------------------------------------------------- 1 | # File: app/models/api_key.py 2 | import secrets 3 | from datetime import datetime 4 | from app import db 5 | from sqlalchemy.dialects.postgresql import UUID 6 | 7 | class ApiKey(db.Model): 8 | __tablename__ = 'api_keys' 9 | 10 | key = db.Column(db.String(80), primary_key=True, unique=True, default=lambda: 'sk-' + secrets.token_urlsafe(50)) 11 | user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False) 12 | user = db.relationship('User', backref=db.backref('api_keys')) 13 | created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 14 | 15 | def __init__(self, user_id): 16 | self.user_id = user_id 17 | 18 | @classmethod 19 | def find_by_key(cls, key): 20 | return cls.query.filter_by(key=key).first() 21 | 22 | def save_to_db(self): 23 | db.session.add(self) 24 | db.session.commit() 25 | 26 | def save(self): 27 | self.save_to_db() 28 | 29 | def delete_from_db(self): 30 | db.session.delete(self) 31 | db.session.commit() 32 | 33 | def json(self): 34 | return { 35 | 'key': self.key, 36 | 'user_id': self.user_id, 37 | 'created_at': self.created_at 38 | } 39 | -------------------------------------------------------------------------------- /server/backend/app/models/invocation.py: -------------------------------------------------------------------------------- 1 | # File: app/models/invocation.py 2 | from app import db, app 3 | import uuid 4 | from sqlalchemy import DateTime, func, text 5 | from sqlalchemy.dialects.postgresql import UUID 6 | from app.models.inference_job import InferenceJob 7 | from app.models.worker import Worker 8 | 9 | class Invocation(db.Model): 10 | __tablename__ = 'invocations' 11 | 12 | id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 13 | user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id')) 14 | inference_job_id = db.Column(UUID(as_uuid=True), db.ForeignKey('inference_jobs.id')) 15 | status = db.Column(db.String(80), default='ENQUEUED') 16 | processed_by_worker_id = db.Column(UUID(as_uuid=True), db.ForeignKey('workers.id')) 17 | 18 | input = db.Column(db.String, default='') 19 | error = db.Column(db.String) 20 | result = db.Column(db.String) 21 | created_at = db.Column(DateTime(timezone=True), default=func.now()) # new created_at field 22 | updated_at = db.Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) # new updated_at field 23 | process_start_time = db.Column(DateTime(timezone=True)) 24 | process_finish_time = db.Column(DateTime(timezone=True)) 25 | processed_by_user = db.Column(UUID(as_uuid=True)) 26 | inference_job_name = db.Column(db.String(80)) 27 | 28 | # These relationships can be expensive to query. 29 | user = db.relationship('User', backref=db.backref('invocations')) 30 | inference_job = db.relationship('InferenceJob', backref=db.backref('invocations')) 31 | processed_by_worker = db.relationship('Worker', backref=db.backref('processed_invocations')) 32 | 33 | def get(self): 34 | item = self.to_dict() 35 | worker = Worker.query.filter_by(id=self.processed_by_worker_id).first() 36 | if worker: 37 | item['processed_by_worker_name'] = worker.name 38 | if self.process_start_time: 39 | item['wait_time'] = (self.process_start_time - self.created_at).total_seconds() 40 | if self.process_finish_time: 41 | item['process_time'] = (self.process_finish_time - self.process_start_time).total_seconds() 42 | 43 | def get_item_for_list(self): 44 | return self.to_dict() 45 | 46 | def to_dict(self): 47 | return { 48 | 'id': self.id, 49 | 'user_id': self.user_id, 50 | 'inference_job_id': self.inference_job_id, 51 | 'inference_job_name': self.inference_job_name, 52 | 'status': self.status, 53 | 'processed_by_worker_id': self.processed_by_worker_id, 54 | 'processed_by_worker_name': self.processed_by_worker.name if self.processed_by_worker else '', 55 | 'processed_by_user_id': self.processed_by_user, 56 | 'input': self.input, 57 | 'error': self.error, 58 | 'result': self.result, 59 | 'created_at': self.created_at, 60 | 'updated_at': self.updated_at, 61 | 'process_start_time': self.process_start_time, 62 | 'process_finish_time': self.process_finish_time, 63 | } 64 | 65 | def delete_from_db(self): 66 | db.session.delete(self) 67 | db.session.commit() 68 | 69 | def delete(self): 70 | self.delete_from_db() 71 | 72 | def __init__(self, inference_job_id, user_id, status=None, input=None, result=None): 73 | self.inference_job_id = inference_job_id 74 | self.user_id = user_id 75 | self.status = status 76 | self.input = input 77 | self.result = result 78 | self.inference_job_name = self.get_inference_job().job_name 79 | 80 | def get_inference_job(self): 81 | return InferenceJob.query.filter_by(id=self.inference_job_id).first() 82 | 83 | def save_to_db(self): 84 | db.session.add(self) 85 | db.session.commit() 86 | 87 | def save(self): 88 | self.save_to_db() 89 | 90 | def enqueue(self): 91 | self.status = 'ENQUEUED' 92 | app.r.lpush(f'job:{self.inference_job_id}', str(self.id)) 93 | self.save_to_db() 94 | 95 | @classmethod 96 | def count_user_invocations_last_min(cls, user_id): 97 | return cls.query.filter_by(user_id=user_id).filter(cls.created_at >= func.now() - text("interval '1 minute'")).count() 98 | 99 | @classmethod 100 | def find(cls, id): 101 | return cls.query.get(id) 102 | -------------------------------------------------------------------------------- /server/backend/app/models/user.py: -------------------------------------------------------------------------------- 1 | # File: app/models/user.py 2 | from app import db 3 | import uuid 4 | from sqlalchemy import DateTime, func 5 | from sqlalchemy.dialects.postgresql import UUID 6 | from werkzeug.security import generate_password_hash, check_password_hash 7 | 8 | class User(db.Model): 9 | __tablename__ = 'users' 10 | 11 | id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 12 | name = db.Column(db.String(80), nullable=False) 13 | email = db.Column(db.String(120), unique=True, nullable=False) 14 | phone = db.Column(db.String(20), nullable=True) 15 | company = db.Column(db.String(80), nullable=True) 16 | password_hash = db.Column(db.String(128)) # New password field 17 | created_at = db.Column(DateTime(timezone=True), default=func.now()) # new created_at field 18 | updated_at = db.Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) # new updated_at field 19 | verified_at = db.Column(DateTime(timezone=True), nullable=True) 20 | 21 | def __init__(self, name, email, password, phone=None, company=None): 22 | self.name = name 23 | self.email = email 24 | self.phone = phone 25 | self.company = company 26 | self.password_hash = generate_password_hash(password) # Hashing the password 27 | 28 | def check_password(self, password): 29 | return check_password_hash(self.password_hash, password) # Check if the password matches 30 | 31 | def json(self): 32 | return { 33 | 'id': self.id, 34 | 'name': self.name, 35 | 'email': self.email, 36 | 'phone': self.phone, 37 | 'company': self.company, 38 | 'created_at': self.created_at, 39 | 'updated_at': self.updated_at, 40 | 'verified_at': self.verified_at, 41 | } 42 | 43 | def delete_all_resources(self): 44 | for api_key in self.api_keys: 45 | api_key.delete_from_db() 46 | for worker in self.workers: 47 | worker.delete_from_db() 48 | for inference_job in self.inference_jobs: 49 | inference_job.delete_from_db() 50 | for model in self.models: 51 | model.delete_from_db() 52 | 53 | def save_to_db(self): 54 | db.session.add(self) 55 | db.session.commit() 56 | 57 | def save(self): 58 | self.save_to_db() 59 | 60 | def delete_from_db(self): 61 | self.delete_all_resources() 62 | db.session.delete(self) 63 | db.session.commit() 64 | 65 | def delete(self): 66 | self.delete_from_db() 67 | 68 | @classmethod 69 | def find(cls, id): 70 | return cls.query.get(id) 71 | 72 | @classmethod 73 | def find_by_id(cls, _id): 74 | return cls.query.get(_id) 75 | 76 | @classmethod 77 | def find_by_email(cls, email): 78 | return cls.query.filter_by(email=email).first() 79 | -------------------------------------------------------------------------------- /server/backend/app/models/worker.py: -------------------------------------------------------------------------------- 1 | # File: app/models/worker.py 2 | from app import db 3 | import uuid 4 | from sqlalchemy import DateTime, func 5 | from sqlalchemy.dialects.postgresql import UUID 6 | 7 | class Worker(db.Model): 8 | __tablename__ = 'workers' 9 | 10 | id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 11 | name = db.Column(db.String(80)) 12 | status = db.Column(db.String(80)) 13 | connected = db.Column(db.Boolean, default=False) 14 | working_on = db.Column(UUID(as_uuid=True), db.ForeignKey('inference_jobs.id')) 15 | info = db.Column(db.String, default='') 16 | user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False) # new user_id field 17 | user = db.relationship('User', backref=db.backref('workers')) # new relationship 18 | created_at = db.Column(DateTime(timezone=True), default=func.now()) # new created_at field 19 | updated_at = db.Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) # new updated_at field 20 | available_gpu = db.Column(db.Integer, default=0) # new available_gpu field 21 | type = db.Column(db.String(80), default ="longlive") # temp, longlive 22 | job_assignment_type = db.Column(db.String(80), default ="manual") # manual, auto 23 | recent_deployment_failure = db.Column(db.String) 24 | 25 | def __init__(self, name, user_id, status=None, connected=False, working_on=None, info=None): 26 | self.name = name 27 | self.user_id = user_id 28 | self.status = status 29 | self.connected = connected 30 | self.working_on = working_on 31 | self.info = info 32 | 33 | def _save_to_db(self): 34 | db.session.add(self) 35 | db.session.commit() 36 | 37 | def save(self): 38 | errors = self.validate_values() 39 | if errors: 40 | # Raise an exception if there are validation errors 41 | raise ValueError(f"Validation errors: {', '.join(errors)}") 42 | self._save_to_db() 43 | 44 | def refresh(self): 45 | db.session.refresh(self) 46 | 47 | def delete_from_db(self): 48 | db.session.delete(self) 49 | db.session.commit() 50 | 51 | def delete(self): 52 | self.delete_from_db() 53 | 54 | def get(self): 55 | return self.to_dict() 56 | 57 | def to_dict(self): 58 | return { 59 | 'id': self.id, 60 | 'name': self.name, 61 | 'status': self.status, 62 | 'connected': self.connected, 63 | 'working_on': self.working_on, 64 | 'info': self.info, 65 | 'user_id': self.user_id, 66 | 'created_at': self.created_at, 67 | 'updated_at': self.updated_at, 68 | 'available_gpu': self.available_gpu, 69 | 'type': self.type, 70 | 'job_assignment_type': self.job_assignment_type, 71 | 'recent_deployment_failure': self.recent_deployment_failure, 72 | } 73 | 74 | def validate_values(self): 75 | errors = [] 76 | # Validate type 77 | if self.type not in ['temp', 'longlive']: 78 | errors.append("Invalid type.") 79 | # Validate job_assignment_type 80 | if self.job_assignment_type not in ['manual', 'auto']: 81 | errors.append("Invalid job_assignment_type.") 82 | return errors 83 | 84 | @classmethod 85 | def find(cls, id): 86 | return cls.query.get(id) 87 | 88 | @classmethod 89 | def get_workers_waiting_for_a_job(cls, gpu_limit=None): 90 | workers = cls.query.filter_by(working_on=None, connected=True, job_assignment_type="auto").all() 91 | if gpu_limit: 92 | workers = [worker for worker in workers if worker.available_gpu >= gpu_limit] 93 | return workers 94 | 95 | @classmethod 96 | def clean_up_temp_workers(cls): 97 | temp_workers = cls.query.filter_by(type="temp", connected=False).all() 98 | for worker in temp_workers: 99 | worker.delete_from_db() 100 | print(f'Cleaned up {len(temp_workers)} temp workers.') 101 | -------------------------------------------------------------------------------- /server/backend/app/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/backend/app/routes/__init__.py -------------------------------------------------------------------------------- /server/backend/app/routes/api_key_routes.py: -------------------------------------------------------------------------------- 1 | # File: app/routes/api_key_routes.py 2 | from flask import Blueprint, jsonify, app, current_app 3 | from app.models.api_key import ApiKey 4 | from app.decorators import api_key_required 5 | 6 | 7 | bp = Blueprint('api_key_routes', __name__) 8 | 9 | @bp.route('/', methods=['POST'], strict_slashes=False) 10 | @api_key_required 11 | def create_api_key(): 12 | user_id = app.g.user.id 13 | new_api_key = ApiKey(user_id) 14 | new_api_key.save_to_db() 15 | return jsonify(new_api_key.key), 201 16 | 17 | @bp.route('/', methods=['GET'], strict_slashes=False) 18 | @api_key_required 19 | def list_api_keys(): 20 | api_keys = app.g.user.api_keys 21 | return jsonify([api_key.json() for api_key in api_keys]) 22 | 23 | @bp.route('/', methods=['DELETE'], strict_slashes=False) 24 | @api_key_required 25 | def delete_api_key(key): 26 | api_key = ApiKey.find_by_key(key) 27 | if not api_key or api_key.user_id != app.g.user.id: 28 | current_app.logger.warning("You are not authorized to access the resource") 29 | return jsonify({'error': 'You are not authorized to access the resource.'}), 403 30 | 31 | api_key.delete_from_db() 32 | return jsonify({'message': 'API Key deleted'}) 33 | -------------------------------------------------------------------------------- /server/backend/app/routes/invocation_routes.py: -------------------------------------------------------------------------------- 1 | # File: app/routes/invocation_routes.py 2 | from flask import Blueprint, jsonify, g, current_app 3 | from app.models.invocation import Invocation 4 | from app.decorators import api_key_required 5 | from app.utils import is_valid_uuid 6 | 7 | bp = Blueprint('invocation_routes', __name__) 8 | 9 | def check_resource_ownership(id): 10 | if not is_valid_uuid(id): 11 | return None 12 | invocation = Invocation.query.filter_by(id=id).first() 13 | if invocation and invocation.user_id == g.user.id: 14 | return invocation 15 | return None 16 | 17 | @bp.route('/', methods=['GET'], strict_slashes=False) 18 | @api_key_required 19 | def get_invocations(): 20 | invocations = g.user.invocations 21 | return jsonify([invocation.to_dict() for invocation in invocations][::-1]) 22 | 23 | @bp.route('/', methods=['GET'], strict_slashes=False) 24 | @api_key_required 25 | def get_invocation(id): 26 | invocation = check_resource_ownership(id) 27 | if not invocation: 28 | current_app.logger.warning('You are not authorized to access the resource.') 29 | return jsonify({'error': 'You are not authorized to access the resource.'}), 403 30 | 31 | return jsonify(invocation.to_dict()) 32 | -------------------------------------------------------------------------------- /server/backend/app/routes/login_routes.py: -------------------------------------------------------------------------------- 1 | # File: app/routes/login_routes.py 2 | 3 | from flask import Blueprint, request, jsonify, current_app 4 | from app.models.user import User 5 | 6 | bp = Blueprint('login_routes', __name__) 7 | 8 | @bp.route('', methods=['POST'], strict_slashes=False) 9 | def login(): 10 | email = request.json.get('email') 11 | password = request.json.get('password') 12 | 13 | user = User.find_by_email(email) 14 | if not user or not user.check_password(password): 15 | current_app.logger.warning(f'{email} {password} is invalid') 16 | return jsonify({'error': 'Not authorized'}), 403 17 | 18 | api_key = user.api_keys[0].key if user.api_keys else None 19 | return jsonify({'api_key': api_key}) 20 | 21 | -------------------------------------------------------------------------------- /server/backend/app/routes/user_routes.py: -------------------------------------------------------------------------------- 1 | # File: app/routes/user_routes.py 2 | import os 3 | from flask import Blueprint, request, jsonify, app, render_template, current_app 4 | from werkzeug.security import generate_password_hash 5 | from app.models.user import User 6 | from app.models.api_key import ApiKey 7 | from app.decorators import api_key_required 8 | from datetime import datetime 9 | from app.utils import send_verification_email 10 | from app.utils import is_valid_uuid 11 | 12 | bp = Blueprint('user_routes', __name__) 13 | 14 | def check_resource_ownership(id): 15 | if not is_valid_uuid(id): 16 | return None 17 | user = User.query.filter_by(id=id).first() 18 | if user and user.id == app.g.user.id: 19 | return user 20 | return None 21 | 22 | @bp.route('/', methods=['POST'], strict_slashes=False) 23 | def create_user(): 24 | name = request.json.get('name') 25 | email = request.json.get('email') 26 | password = request.json.get('password') # Get password from request 27 | phone = request.json.get('phone') 28 | company = request.json.get('company') 29 | if User.find_by_email(email): 30 | current_app.logger.warning(f"{email} User already exists") 31 | return jsonify({'error': 'User already exists'}), 403 32 | 33 | new_user = User(name, email, password, phone, company) 34 | new_user.save_to_db() 35 | 36 | # Create an API key for the new user 37 | new_api_key = ApiKey(user_id=new_user.id) 38 | new_api_key.save_to_db() 39 | if os.getenv('FLASK_ENV') == 'production': 40 | current_app.logger.info(f"start send verify email for {new_user.email}") 41 | send_verification_email(f'https://console.clustro.ai/api/users/verify/{new_user.id}', new_user.email) 42 | 43 | return jsonify(new_user.id), 201 44 | 45 | @bp.route('/', methods=['GET'], strict_slashes=False) 46 | @api_key_required 47 | def get_single_user(): 48 | return jsonify(app.g.user.json()) 49 | 50 | @bp.route('/', methods=['GET'], strict_slashes=False) 51 | @api_key_required 52 | def get_user(id): 53 | user = check_resource_ownership(id) 54 | if not user: 55 | current_app.logger.warning("get_user You are not authorized to access the resource") 56 | return jsonify({'error': 'You are not authorized to access the resource.'}), 403 57 | return jsonify(user.json()) 58 | 59 | @bp.route('/', methods=['PUT'], strict_slashes=False) 60 | @api_key_required 61 | def update_user(id): 62 | user = check_resource_ownership(id) 63 | if not user: 64 | current_app.logger.warning("update_user You are not authorized to access the resource") 65 | return jsonify({'error': 'You are not authorized to access the resource.'}), 403 66 | 67 | data = request.get_json() 68 | if 'name' in data: 69 | user.name = data['name'] 70 | if 'email' in data: 71 | user.email = data['email'] 72 | if 'phone' in data: 73 | user.phone = data['phone'] 74 | if 'company' in data: 75 | user.company = data['company'] 76 | user.save_to_db() 77 | return jsonify(user.json()) 78 | 79 | @bp.route('/', methods=['DELETE'], strict_slashes=False) 80 | @api_key_required 81 | def delete_user(id): 82 | user = check_resource_ownership(id) 83 | if not user: 84 | current_app.logger.warning("delete_user You are not authorized to access the resource") 85 | return jsonify({'error': 'You are not authorized to access the resource.'}), 403 86 | 87 | user.delete_from_db() 88 | return jsonify({'message': 'User deleted'}) 89 | 90 | @bp.route('/verify/', methods=['GET'], strict_slashes=False) 91 | def verify_user(id): 92 | user = User.query.filter_by(id=id).first() 93 | if not user: 94 | current_app.logger.warning("verify_user You are not authorized to access the resource") 95 | return jsonify({'error': 'You are not authorized to access the resource.'}), 403 96 | if not user.verified_at: 97 | user.verified_at = datetime.utcnow() 98 | user.save_to_db() 99 | return render_template('email_verify.html') 100 | -------------------------------------------------------------------------------- /server/backend/app/routes/worker_routes.py: -------------------------------------------------------------------------------- 1 | # File: app/routes/worker_routes.py 2 | from flask import Blueprint, request, jsonify, g, current_app 3 | from app.models.worker import Worker 4 | from app.decorators import api_key_required 5 | import string 6 | import random 7 | from app.utils import is_valid_uuid 8 | 9 | bp = Blueprint('worker_routes', __name__) 10 | 11 | def check_resource_ownership(id): 12 | if not is_valid_uuid(id): 13 | return None 14 | worker = Worker.query.filter_by(id=id).first() 15 | if worker and worker.user_id == g.user.id: 16 | return worker 17 | return None 18 | 19 | @bp.route('/', methods=['POST'], strict_slashes=False) 20 | @api_key_required 21 | def create_worker(): 22 | if 'name' in request.json: 23 | name = request.json.get('name') 24 | else: 25 | name = _generate_random_string() 26 | new_worker = Worker(name, user_id=g.user.id) 27 | 28 | if 'type' in request.json and request.json.get('type') == 'temp': 29 | new_worker.type = 'temp' 30 | else: 31 | new_worker.type = 'longlive' 32 | 33 | if 'job_assignment_type' in request.json and request.json.get('job_assignment_type') == 'manual': 34 | new_worker.job_assignment_type = 'manual' 35 | else: 36 | new_worker.job_assignment_type = 'auto' 37 | 38 | if 'available_gpu' in request.json: 39 | new_worker.available_gpu = int(request.json.get('available_gpu')) 40 | else: 41 | new_worker.available_gpu = 0 42 | 43 | new_worker.save() 44 | return jsonify(new_worker.get()), 201 45 | 46 | @bp.route('/', methods=['GET'], strict_slashes=False) 47 | @api_key_required 48 | def get_worker(id): 49 | worker = check_resource_ownership(id) 50 | if not worker: 51 | current_app.logger.warning("get_worker You are not authorized to access the resource") 52 | return jsonify({'error': 'You are not authorized to access the resource.'}), 403 53 | 54 | return jsonify(worker.to_dict()) 55 | 56 | @bp.route('/', methods=['GET'], strict_slashes=False) 57 | @api_key_required 58 | def list_workers(): 59 | workers = g.user.workers 60 | return jsonify([worker.to_dict() for worker in workers]) 61 | 62 | @bp.route('/', methods=['PUT'], strict_slashes=False) 63 | @api_key_required 64 | def update_worker(id): 65 | worker = check_resource_ownership(id) 66 | if not worker: 67 | current_app.logger.warning("update_worker You are not authorized to access the resource") 68 | return jsonify({'error': 'You are not authorized to access the resource.'}), 403 69 | 70 | data = request.get_json() 71 | if 'working_on' in data: 72 | worker.working_on = data['working_on'] 73 | if data['working_on'] == "": 74 | worker.working_on = None 75 | if 'job_assignment_type' in data: 76 | worker.job_assignment_type = data['job_assignment_type'] 77 | if 'type' in data: 78 | worker.type = data['type'] 79 | 80 | try: 81 | worker.save() 82 | return jsonify(worker.get()) 83 | except ValueError as e: 84 | return jsonify({'error': str(e)}), 400 85 | 86 | @bp.route('/', methods=['DELETE'], strict_slashes=False) 87 | @api_key_required 88 | def delete_worker(id): 89 | worker = check_resource_ownership(id) 90 | if not worker: 91 | current_app.logger.warning("delete_worker You are not authorized to access the resource") 92 | return jsonify({'error': 'You are not authorized to access the resource.'}), 403 93 | worker.delete_from_db() 94 | return jsonify({'message': 'Worker deleted'}) 95 | 96 | def _generate_random_string(length=8): 97 | characters = string.ascii_lowercase + string.digits # includes lowercase letters and numbers 98 | return ''.join(random.choice(characters) for _ in range(length)) -------------------------------------------------------------------------------- /server/backend/app/utils.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import base64 4 | import os 5 | import logging 6 | from sendgrid import SendGridAPIClient 7 | from sendgrid.helpers.mail import Mail 8 | import uuid 9 | 10 | logger = logging.getLogger('app') 11 | bucket_name = "clustroai-s3-dev" 12 | path = "generated-data/" 13 | 14 | 15 | def create_presigned_fields(invocation_id, expiration=3600): 16 | object_name = path + invocation_id + '.png' 17 | s3_client = boto3.client('s3') 18 | fields = s3_client.generate_presigned_post(Bucket=bucket_name, 19 | Key=object_name, 20 | ExpiresIn=expiration) 21 | fields["Content-Type"] = "image/png" 22 | return base64.b64encode(json.dumps(fields).encode('utf-8')).decode() 23 | 24 | 25 | def is_valid_uuid(s): 26 | try: 27 | uuid.UUID(s) 28 | return True 29 | except ValueError: 30 | return False 31 | 32 | 33 | # using SendGrid's Python Library 34 | # https://github.com/sendgrid/sendgrid-python 35 | 36 | def send_verification_email(verification_link, email): 37 | message = Mail( 38 | from_email='admin@clustro.ai', 39 | to_emails=email, 40 | subject='ClustroAI verification', 41 | html_content='Click here to verify your email.') 42 | try: 43 | sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) 44 | response = sg.send(message) 45 | return response 46 | except Exception as e: 47 | logger.error(str(e)) 48 | -------------------------------------------------------------------------------- /server/backend/config.py: -------------------------------------------------------------------------------- 1 | # File: app/config.py 2 | import os 3 | import redis 4 | 5 | 6 | class Config(object): 7 | DEBUG = False 8 | TESTING = False 9 | SQLALCHEMY_DATABASE_URI = f'postgresql://{os.getenv("DB_USER")}:{os.getenv("DB_PASSWORD")}@localhost/clustroai' 10 | REDIS_HOST = os.getenv('REDIS_HOST', 'localhost') 11 | 12 | 13 | class ProductionConfig(Config): 14 | SQLALCHEMY_DATABASE_URI = f'postgresql://{os.getenv("DB_USER")}:{os.getenv("DB_PASSWORD")}@database-1.cojaa9n9bkqt.us-east-1.rds.amazonaws.com/clustroai' 15 | SESSION_TYPE = 'redis' 16 | SESSION_PERMANENT = False 17 | SESSION_USE_SIGNER = True 18 | SESSION_KEY_PREFIX = 'session:' 19 | SESSION_REDIS = redis.StrictRedis(host=SQLALCHEMY_DATABASE_URI, port=6379, db=0) 20 | 21 | 22 | class DevelopmentConfig(Config): 23 | # SQLALCHEMY_DATABASE_URI = f'postgresql://{os.getenv("DB_USER")}:{os.getenv("DB_PASSWORD")}@database-1.cojaa9n9bkqt.us-east-1.rds.amazonaws.com/clustroai' 24 | SQLALCHEMY_DATABASE_URI = f'postgresql://{os.getenv("DB_USER")}:{os.getenv("DB_PASSWORD")}@{os.getenv("DB_HOST", "database-1.cojaa9n9bkqt.us-east-1.rds.amazonaws.com")}/clustroai' 25 | SESSION_TYPE = 'redis' 26 | SESSION_PERMANENT = False 27 | SESSION_USE_SIGNER = True 28 | SESSION_KEY_PREFIX = 'session:' 29 | SESSION_REDIS = redis.StrictRedis(host=SQLALCHEMY_DATABASE_URI, port=6379, db=0) 30 | DEBUG = True 31 | 32 | 33 | class TestingConfig(Config): 34 | TESTING = True 35 | -------------------------------------------------------------------------------- /server/backend/create_seed_data.py: -------------------------------------------------------------------------------- 1 | from app import db, app 2 | from app.models.inference_job import InferenceJob 3 | from app.models.worker import Worker 4 | from app.models.api_key import ApiKey 5 | from app.models.user import User 6 | from app.models.model import Model 7 | 8 | def create_seed_data(): 9 | if User.query.filter_by(email="demo@example.com").first(): 10 | demo_user = User.query.filter_by(email="demo@example.com").first() 11 | api_key = demo_user.api_keys[0] 12 | model1 = demo_user.models[0] 13 | model2 = demo_user.models[1] 14 | inference_job1 = demo_user.inference_jobs[0] 15 | inference_job2 = demo_user.inference_jobs[1] 16 | worker1 = demo_user.workers[0] 17 | worker2 = demo_user.workers[1] 18 | 19 | else: 20 | # Create a user named "demo" with password "demoAdmin@1234" 21 | demo_user = User( 22 | name="demo", 23 | email="demo@example.com", 24 | password="demoAdmin@1234" 25 | # Add other user details here 26 | ) 27 | db.session.add(demo_user) 28 | db.session.commit() 29 | 30 | # Create an API key for the user 31 | api_key = ApiKey( 32 | user_id=demo_user.id 33 | ) 34 | db.session.add(api_key) 35 | db.session.commit() 36 | 37 | # Create two models belonging to the user 38 | model1 = Model( 39 | name="llama7b", 40 | version="1.0", 41 | user_id=demo_user.id, 42 | model_type="text_to_text", 43 | artifact="https://huggingface.co/tiiuae/falcon-7b" 44 | # Add other model details here 45 | ) 46 | db.session.add(model1) 47 | 48 | model2 = Model( 49 | name="stablediffusion", 50 | version="2.1", 51 | user_id=demo_user.id, 52 | model_type="text_to_image", 53 | artifact="https://huggingface.co/stabilityai/stable-diffusion-2-1" 54 | # Add other model details here 55 | ) 56 | db.session.add(model2) 57 | db.session.commit() 58 | 59 | # Create two inference jobs, one for each model 60 | inference_job1 = InferenceJob( 61 | job_name="inference_job_1_falcon7b", 62 | status="pending", 63 | enabled=True, 64 | model_id=model1.id, 65 | user_id=demo_user.id 66 | # Add other inference job details here 67 | ) 68 | db.session.add(inference_job1) 69 | 70 | inference_job2 = InferenceJob( 71 | job_name="inference_job_2_sd", 72 | status="pending", 73 | enabled=True, 74 | model_id=model2.id, 75 | user_id=demo_user.id 76 | # Add other inference job details here 77 | ) 78 | db.session.add(inference_job2) 79 | db.session.commit() 80 | 81 | # Create two workers, one for each inference job 82 | worker1 = Worker( 83 | name="worker_1_llm", 84 | status="active", 85 | connected=True, 86 | working_on=inference_job1.id, 87 | user_id=demo_user.id 88 | # Add other worker details here 89 | ) 90 | db.session.add(worker1) 91 | 92 | worker2 = Worker( 93 | name="worker_2_llm", 94 | status="active", 95 | connected=True, 96 | working_on=inference_job1.id, 97 | user_id=demo_user.id 98 | # Add other worker details here 99 | ) 100 | db.session.add(worker2) 101 | db.session.commit() 102 | 103 | worker3 = Worker( 104 | name="worker_3_sd", 105 | status="active", 106 | connected=True, 107 | working_on=inference_job2.id, 108 | user_id=demo_user.id 109 | # Add other worker details here 110 | ) 111 | db.session.add(worker3) 112 | db.session.commit() 113 | 114 | # Print the objects with generated UUID values 115 | print("User UUID:", demo_user.id) 116 | print("API Key UUID:", api_key.key) 117 | print("") 118 | print("Model 1 UUID:", model1.id) 119 | print("Inference Job 1 UUID:", inference_job1.id) 120 | print("Worker 1 UUID:", worker1.id) 121 | print("") 122 | print("Model 2 UUID:", model2.id) 123 | print("Inference Job 2 UUID:", inference_job2.id) 124 | print("Worker 2 UUID:", worker2.id) 125 | print("") 126 | 127 | with app.app_context(): 128 | create_seed_data() 129 | -------------------------------------------------------------------------------- /server/backend/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore:pkg_resources is deprecated:DeprecationWarning 4 | 5 | -------------------------------------------------------------------------------- /server/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.2 2 | Flask-SQLAlchemy==3.0.3 3 | psycopg2-binary==2.9.6 4 | redis==4.5.5 5 | Flask-SocketIO==5.3.4 6 | simple-websocket==0.10.1 7 | flask-cors==3.0.10 8 | Flask-Migrate==4.0.4 9 | pytest==7.3.2 10 | pytest-xdist==3.3.1 11 | Gunicorn==20.1.0 12 | setuptools 13 | boto3 14 | sendgrid 15 | Flask-APScheduler -------------------------------------------------------------------------------- /server/backend/run.py: -------------------------------------------------------------------------------- 1 | # File: run.py 2 | import os 3 | from app import app, db, socketio 4 | from app.load_jobs import load_invocations_to_redis 5 | 6 | from flask_apscheduler import APScheduler 7 | from app.job_matcher import match_jobs 8 | from app.auto_scaler import auto_scale 9 | 10 | if __name__ == '__main__': 11 | # Initialize and configure scheduler 12 | scheduler = APScheduler() 13 | scheduler.init_app(app) 14 | scheduler.start() 15 | 16 | 17 | @scheduler.task('interval', id='do_job_matching', seconds=5, misfire_grace_time=900) 18 | def scheduled_match_jobs(): 19 | # print("Starting job matching...") 20 | with app.app_context(): 21 | match_jobs() 22 | 23 | 24 | @scheduler.task('interval', id='auto_scaling', seconds=10, misfire_grace_time=900) 25 | def scheduled_match_jobs(): 26 | with app.app_context(): 27 | auto_scale() 28 | 29 | # This is only used when running locally 30 | 31 | 32 | with app.app_context(): 33 | db.create_all() 34 | try: 35 | load_invocations_to_redis() 36 | except: 37 | pass 38 | 39 | socketio.run(app, host='0.0.0.0', port=os.getenv("PORT", 5000), allow_unsafe_werkzeug=True) 40 | -------------------------------------------------------------------------------- /server/backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # tests/__init__.py 2 | -------------------------------------------------------------------------------- /server/backend/tests/test_api_key_routes.py: -------------------------------------------------------------------------------- 1 | # File: tests/test_api_key_routes.py 2 | import pytest 3 | from flask import json 4 | from app import create_app, db 5 | from app.models.user import User 6 | from app.models.api_key import ApiKey 7 | import random 8 | import string 9 | 10 | 11 | def random_email(): 12 | # Generates a random email address by appending a random string to a base email 13 | return ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + '@example.com' 14 | 15 | 16 | @pytest.fixture(scope='module') 17 | def client(): 18 | flask_app = create_app('config.TestingConfig') 19 | from app.routes import api_key_routes 20 | flask_app.register_blueprint(api_key_routes.bp, url_prefix='/api_keys') 21 | 22 | testing_client = flask_app.test_client() 23 | with flask_app.app_context(): 24 | db.create_all() 25 | # Establish an application context before running the tests. 26 | ctx = flask_app.app_context() 27 | ctx.push() 28 | 29 | yield testing_client # this is where the testing happens! 30 | 31 | ctx.pop() 32 | 33 | 34 | @pytest.fixture 35 | def api_key(client): 36 | db.session.begin_nested() 37 | 38 | # Use the random_email function to generate a unique email address for each user 39 | user_email = random_email() 40 | user = User(name='Test User', email=user_email, password='test') 41 | user.save_to_db() 42 | api_key = ApiKey(user_id=user.id) 43 | api_key.save_to_db() 44 | 45 | yield api_key.key 46 | 47 | # After the test, delete the created user and api key 48 | for api_key in user.api_keys: 49 | api_key.delete_from_db() 50 | db.session.commit() 51 | user.delete_from_db() 52 | db.session.commit() 53 | 54 | db.session.rollback() 55 | return api_key.key 56 | 57 | 58 | def test_create_api_key(client, api_key): 59 | headers = {'X-API-Key': api_key} 60 | response = client.post('/api_keys', headers=headers) 61 | assert response.status_code == 201 62 | 63 | 64 | def test_list_api_keys(client, api_key): 65 | headers = {'X-API-Key': api_key} 66 | response = client.get('/api_keys', headers=headers) 67 | assert response.status_code == 200 68 | data = json.loads(response.data) 69 | assert isinstance(data, list) 70 | 71 | 72 | def test_delete_api_key(client, api_key): 73 | headers = {'X-API-Key': api_key} 74 | response = client.delete(f'/api_keys/{api_key}', headers=headers) 75 | assert response.status_code == 200 76 | data = json.loads(response.data) 77 | assert data['message'] == 'API Key deleted' 78 | response = client.get('/api_keys', headers=headers) 79 | assert response.status_code == 403 80 | 81 | 82 | def test_create_api_key_invalid_api_key(client): 83 | headers = {'X-API-Key': 'invalid_api_key'} 84 | response = client.post('/api_keys', headers=headers) 85 | assert response.status_code == 403 86 | data = json.loads(response.data) 87 | assert 'error' in data 88 | 89 | 90 | def test_delete_api_key_unauthorized(client, api_key): 91 | headers = {'X-API-Key': api_key} 92 | # Create a different user with a new API key 93 | unauthorized_user = User(name='Unauthorized User', email='unauthorized@example.com', password='test') 94 | unauthorized_user.save_to_db() 95 | unauthorized_api_key = ApiKey(user_id=unauthorized_user.id) 96 | unauthorized_api_key.save_to_db() 97 | 98 | response = client.delete(f'/api_keys/{unauthorized_api_key.key}', headers=headers) 99 | assert response.status_code == 403 100 | data = json.loads(response.data) 101 | assert 'error' in data 102 | 103 | # Clean up the unauthorized user and API key 104 | unauthorized_api_key.delete_from_db() 105 | unauthorized_user.delete_from_db() 106 | db.session.commit() 107 | 108 | 109 | def test_delete_api_key_not_found(client, api_key): 110 | headers = {'X-API-Key': api_key} 111 | invalid_key = 'invalid_api_key' 112 | 113 | response = client.delete(f'/api_keys/{invalid_key}', headers=headers) 114 | assert response.status_code == 403 115 | data = json.loads(response.data) 116 | assert 'error' in data 117 | -------------------------------------------------------------------------------- /server/datadog/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcr.io/datadoghq/agent:latest 2 | ADD conf.d/redisdb.yaml /etc/datadog-agent/conf.d/redisdb.yaml 3 | ADD conf.d/frontend.yaml /etc/datadog-agent/conf.d/frontend.yaml 4 | ADD conf.d/nginx.yaml /etc/datadog-agent/conf.d/nginx.yaml 5 | ADD conf.d/backend.yaml /etc/datadog-agent/conf.d/backend.yaml 6 | -------------------------------------------------------------------------------- /server/datadog/conf.d/backend.yaml: -------------------------------------------------------------------------------- 1 | init_config: 2 | 3 | instances: 4 | - host: backend 5 | port: 5000 6 | -------------------------------------------------------------------------------- /server/datadog/conf.d/frontend.yaml: -------------------------------------------------------------------------------- 1 | init_config: 2 | 3 | instances: 4 | - host: frontend 5 | port: 3000 6 | -------------------------------------------------------------------------------- /server/datadog/conf.d/nginx.yaml: -------------------------------------------------------------------------------- 1 | init_config: 2 | 3 | instances: 4 | - host: nginx 5 | port: 443 6 | -------------------------------------------------------------------------------- /server/datadog/conf.d/redisdb.yaml: -------------------------------------------------------------------------------- 1 | init_config: 2 | 3 | instances: 4 | - host: redis 5 | port: 6379 6 | -------------------------------------------------------------------------------- /server/docker-compose-staging.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | backend: 5 | build: 6 | context: ./backend 7 | labels: 8 | com.datadoghq.ad.logs: '[{"source": "backend", "service": "backend"}]' 9 | volumes: 10 | - ./backend:/backend 11 | environment: 12 | - REDIS_HOST=redis 13 | - DB_USER=${DB_USER} 14 | - DB_PASSWORD=${DB_PASSWORD} 15 | - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 16 | - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} 17 | - SENDGRID_API_KEY=${SENDGRID_API_KEY} 18 | ports: 19 | - "5000:5000" 20 | expose: 21 | - 5000 22 | depends_on: 23 | - redis 24 | restart: always 25 | 26 | frontend: 27 | build: 28 | context: ./frontend 29 | labels: 30 | com.datadoghq.ad.logs: '[{"source": "frontend", "service": "frontend"}]' 31 | volumes: 32 | - ./frontend:/frontend 33 | ports: 34 | - "3000:3000" 35 | expose: 36 | - 3000 37 | restart: always 38 | 39 | landingpage: 40 | build: 41 | context: ./landingPage 42 | volumes: 43 | - ./landingPage:/landingPage 44 | expose: 45 | - 3000 46 | restart: always 47 | 48 | nginx: 49 | image: nginx:latest 50 | labels: 51 | com.datadoghq.ad.logs: '[{"source": "nginx", "service": "nginx"}]' 52 | volumes: 53 | - ./nginx.conf:/etc/nginx/nginx.conf 54 | - ./cert.pem:/etc/nginx/cert.pem 55 | - ./privkey.pem:/etc/nginx/privkey.pem 56 | ports: 57 | - "80:80" 58 | - "443:443" 59 | depends_on: 60 | - backend 61 | - frontend 62 | restart: always 63 | 64 | redis: 65 | image: redis:latest 66 | labels: 67 | com.datadoghq.ad.logs: '[{"source": "redis", "service": "redis"}]' 68 | expose: 69 | - 6379 70 | restart: always 71 | 72 | datadog: 73 | build: datadog 74 | pid: host 75 | environment: 76 | - DD_API_KEY=86ca18de8b44a8d31631ab3321a64308 77 | - DD_SITE=us5.datadoghq.com 78 | - DD_LOGS_ENABLED=true 79 | volumes: 80 | - /var/run/docker.sock:/var/run/docker.sock 81 | - /proc/:/host/proc/:ro 82 | - /sys/fs/cgroup:/host/sys/fs/cgroup:ro 83 | - /var/lib/docker/containers:/var/lib/docker/containers:ro 84 | depends_on: 85 | - backend 86 | - frontend 87 | - nginx 88 | - redis 89 | 90 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | backend: 4 | build: 5 | context: ./backend 6 | volumes: 7 | - ./backend:/backend 8 | environment: 9 | - REDIS_HOST=redis 10 | - DB_USER=${DB_USER} 11 | - DB_PASSWORD=${DB_PASSWORD} 12 | - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 13 | - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} 14 | - SENDGRID_API_KEY=${SENDGRID_API_KEY} 15 | ports: 16 | - "5000:5000" 17 | expose: 18 | - 5000 19 | depends_on: 20 | - redis 21 | restart: always 22 | 23 | frontend: 24 | build: 25 | context: ./frontend 26 | volumes: 27 | - ./frontend:/frontend 28 | ports: 29 | - "3000:3000" 30 | expose: 31 | - 3000 32 | restart: always 33 | 34 | landingpage: 35 | build: 36 | context: ./landingPage 37 | volumes: 38 | - ./landingPage:/landingPage 39 | expose: 40 | - 3000 41 | restart: always 42 | 43 | nginx: 44 | image: nginx:latest 45 | volumes: 46 | - ./nginx.conf:/etc/nginx/nginx.conf 47 | - ./cert.pem:/etc/nginx/cert.pem 48 | - ./privkey.pem:/etc/nginx/privkey.pem 49 | ports: 50 | - "80:80" 51 | - "443:443" 52 | depends_on: 53 | - backend 54 | - frontend 55 | restart: always 56 | 57 | 58 | redis: 59 | image: redis:latest 60 | expose: 61 | - 6379 62 | restart: always 63 | -------------------------------------------------------------------------------- /server/docker-compose_frontend_dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | backend: 4 | build: 5 | context: ./backend 6 | volumes: 7 | - ./backend:/backend 8 | environment: 9 | - REDIS_HOST=redis 10 | - DB_USER=${DB_USER} 11 | - DB_PASSWORD=${DB_PASSWORD} 12 | - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 13 | - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} 14 | - SENDGRID_API_KEY=${SENDGRID_API_KEY} 15 | ports: 16 | - "5000:5000" 17 | expose: 18 | - 5000 19 | depends_on: 20 | - redis 21 | restart: always 22 | 23 | redis: 24 | image: redis:latest 25 | expose: 26 | - 6379 27 | restart: always 28 | -------------------------------------------------------------------------------- /server/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js runtime as a parent image 2 | FROM node:14.17.0-alpine3.13 as build 3 | 4 | # Set the working directory to /app 5 | WORKDIR /app 6 | 7 | # Copy the package.json and package-lock.json files to the container 8 | COPY *.* ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the rest of the application code to the container 14 | COPY . . 15 | 16 | # Build the application 17 | RUN npm run build 18 | 19 | FROM nginx:alpine 20 | 21 | # Copy the Nginx configuration file 22 | COPY nginx.conf /etc/nginx/nginx.conf 23 | 24 | # Copy the build output from the previous stage 25 | COPY --from=build /app/dist /usr/share/nginx/html 26 | 27 | EXPOSE 3000 28 | 29 | CMD ["nginx", "-g", "daemon off;"] 30 | -------------------------------------------------------------------------------- /server/frontend/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | `@umijs/max` 模板项目,更多功能参考 [Umi Max 简介](https://umijs.org/docs/max/introduce) 4 | -------------------------------------------------------------------------------- /server/frontend/env_template: -------------------------------------------------------------------------------- 1 | # move this to .env 2 | REACT_APP_BACKEND_SERVER_URL=http://localhost:5000 3 | -------------------------------------------------------------------------------- /server/frontend/mock/userAPI.ts: -------------------------------------------------------------------------------- 1 | const users = [ 2 | { id: 0, name: 'Umi', nickName: 'U', gender: 'MALE' }, 3 | { id: 1, name: 'Fish', nickName: 'B', gender: 'FEMALE' }, 4 | ]; 5 | 6 | export default { 7 | 'GET /api/v1/queryUserList': (req: any, res: any) => { 8 | res.json({ 9 | success: true, 10 | data: { list: users }, 11 | errorCode: 0, 12 | }); 13 | }, 14 | 'PUT /api/v1/user/': (req: any, res: any) => { 15 | res.json({ 16 | success: true, 17 | errorCode: 0, 18 | }); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /server/frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | events {} 2 | 3 | http { 4 | include mime.types; 5 | default_type application/octet-stream; 6 | 7 | server { 8 | listen 3000; 9 | 10 | location / { 11 | root /usr/share/nginx/html; 12 | index index.html index.htm; 13 | try_files $uri $uri/ /index.html; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /server/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "author": "zhangbaibing ", 4 | "scripts": { 5 | "dev": "max dev", 6 | "build": "max build", 7 | "format": "prettier --cache --write .", 8 | "prepare": "husky install", 9 | "postinstall": "max setup", 10 | "setup": "max setup", 11 | "start": "npm run dev" 12 | }, 13 | "dependencies": { 14 | "@ant-design/icons": "^5.0.1", 15 | "@ant-design/pro-components": "^2.4.4", 16 | "@umijs/max": "^4.0.72", 17 | "antd": "^5.4.0", 18 | "max": "^0.0.1" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.0.33", 22 | "@types/react-dom": "^18.0.11", 23 | "husky": "^8.0.3", 24 | "lint-staged": "^13.2.0", 25 | "prettier": "^2.8.7", 26 | "prettier-plugin-organize-imports": "^3.2.2", 27 | "prettier-plugin-packagejson": "^2.4.3", 28 | "tailwindcss": "^3", 29 | "typescript": "^5.0.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/frontend/public/favicon.ico -------------------------------------------------------------------------------- /server/frontend/src/access.ts: -------------------------------------------------------------------------------- 1 | export default (initialState: API.UserInfo) => { 2 | // 在这里按照初始化数据定义项目中的权限,统一管理 3 | // 参考文档 https://umijs.org/docs/max/access 4 | const canSeeAdmin = !!( 5 | initialState && initialState.name !== 'dontHaveAccess' 6 | ); 7 | return { 8 | canSeeAdmin, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /server/frontend/src/app.tsx: -------------------------------------------------------------------------------- 1 | // 运行时配置 2 | import { history, useModel, useRequest } from "@umijs/max"; 3 | import iconLogo from './assets/logo.png'; 4 | import * as services from './services/global'; 5 | import { useEffect } from "react"; 6 | import UserAction from './components/UserAction'; 7 | import { message } from 'antd'; 8 | 9 | message.config({ 10 | duration: 3, 11 | maxCount: 1, 12 | }); 13 | 14 | 15 | // 全局初始化数据配置,用于 Layout 用户信息和权限初始化 16 | // 更多信息见文档:https://umijs.org/docs/apiruntime-config#getinitialstate 17 | export async function getInitialState(): Promise<{ 18 | name?: string, 19 | user?: object, 20 | models?: any[], 21 | jobs?: any[], 22 | invocations?: any[], 23 | workers?: any[], 24 | fetchGlobalData?: () => void, 25 | }> { 26 | 27 | 28 | if (window.location.pathname.indexOf('/user/') === -1) { 29 | if (!localStorage.token) { 30 | history.push('/user/login') 31 | return {}; 32 | } 33 | } 34 | 35 | return {}; 36 | } 37 | 38 | export const layout = ({ initialState }) => { 39 | return { 40 | logo: iconLogo, 41 | menu: { 42 | locale: false, 43 | }, 44 | rightRender: () => , 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /server/frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /server/frontend/src/components/Guide/Guide.less: -------------------------------------------------------------------------------- 1 | .title { 2 | margin: 0 auto; 3 | font-weight: 200; 4 | } 5 | -------------------------------------------------------------------------------- /server/frontend/src/components/Guide/Guide.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, Row, Typography } from 'antd'; 2 | import React from 'react'; 3 | import styles from './Guide.less'; 4 | 5 | interface Props { 6 | name: string; 7 | } 8 | 9 | // 脚手架示例组件 10 | const Guide: React.FC = (props) => { 11 | const { name } = props; 12 | return ( 13 | 14 | 15 | 16 | 欢迎使用 {name} ! 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default Guide; 24 | -------------------------------------------------------------------------------- /server/frontend/src/components/Guide/index.ts: -------------------------------------------------------------------------------- 1 | import Guide from './Guide'; 2 | export default Guide; 3 | -------------------------------------------------------------------------------- /server/frontend/src/components/UserAction/index.less: -------------------------------------------------------------------------------- 1 | .ant-pro-sider-actions { 2 | display: block !important; 3 | } -------------------------------------------------------------------------------- /server/frontend/src/components/UserAction/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Dropdown, Menu, Avatar } from 'antd'; 3 | import { history, useModel } from "@umijs/max"; 4 | 5 | 6 | import './index.less'; 7 | 8 | const UserAction = () => { 9 | const { getUsers } = useModel('global'); 10 | 11 | const users = getUsers.data; 12 | 13 | const { name } = users || {}; 14 | 15 | const menus = [ 16 | { 17 | key: 'logout', 18 | label: 'Logout', 19 | onClick() { 20 | localStorage.token = ''; 21 | history.push('/user/login') 22 | } 23 | } 24 | ] 25 | 26 | const dropdownProps = { 27 | overlay: ( 28 | 29 | {menus.map((item) => ( 30 | 31 | {item.label} 32 | 33 | ))} 34 | 35 | ), 36 | } 37 | 38 | return ( 39 |
40 | 41 |
42 | 50 |
51 | {name} 52 |
53 |
54 |
55 |
56 | ) 57 | } 58 | 59 | export default UserAction; -------------------------------------------------------------------------------- /server/frontend/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_NAME = 'Umi Max'; 2 | -------------------------------------------------------------------------------- /server/frontend/src/global.css: -------------------------------------------------------------------------------- 1 | html { 2 | --ant-primary-color: #000; 3 | --ant-primary-color-hover: #40a9ff; 4 | --ant-primary-color-active: #096dd9; 5 | --ant-primary-color-outline: rgba(24, 144, 255, 0.2); 6 | --ant-primary-1: #e6f7ff; 7 | --ant-primary-2: #bae7ff; 8 | --ant-primary-3: #91d5ff; 9 | --ant-primary-4: #69c0ff; 10 | --ant-primary-5: #40a9ff; 11 | --ant-primary-6: #1890ff; 12 | --ant-primary-7: #096dd9; 13 | --ant-primary-color-deprecated-pure: ; 14 | --ant-primary-color-deprecated-l-35: #cbe6ff; 15 | --ant-primary-color-deprecated-l-20: #7ec1ff; 16 | --ant-primary-color-deprecated-t-20: #46a6ff; 17 | --ant-primary-color-deprecated-t-50: #8cc8ff; 18 | --ant-primary-color-deprecated-f-12: rgba(24, 144, 255, 0.12); 19 | --ant-primary-color-active-deprecated-f-30: rgba(230, 247, 255, 0.3); 20 | --ant-primary-color-active-deprecated-d-02: #dcf4ff; 21 | --ant-success-color: #52c41a; 22 | --ant-success-color-hover: #73d13d; 23 | --ant-success-color-active: #389e0d; 24 | --ant-success-color-outline: rgba(82, 196, 26, 0.2); 25 | --ant-success-color-deprecated-bg: #f6ffed; 26 | --ant-success-color-deprecated-border: #b7eb8f; 27 | --ant-error-color: #ff4d4f; 28 | --ant-error-color-hover: #ff7875; 29 | --ant-error-color-active: #d9363e; 30 | --ant-error-color-outline: rgba(255, 77, 79, 0.2); 31 | --ant-error-color-deprecated-bg: #fff2f0; 32 | --ant-error-color-deprecated-border: #ffccc7; 33 | --ant-warning-color: #faad14; 34 | --ant-warning-color-hover: #ffc53d; 35 | --ant-warning-color-active: #d48806; 36 | --ant-warning-color-outline: rgba(250, 173, 20, 0.2); 37 | --ant-warning-color-deprecated-bg: #fffbe6; 38 | --ant-warning-color-deprecated-border: #ffe58f; 39 | --ant-info-color: #1890ff; 40 | --ant-info-color-deprecated-bg: #e6f7ff; 41 | --ant-info-color-deprecated-border: #91d5ff; 42 | } 43 | -------------------------------------------------------------------------------- /server/frontend/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useColumnsState } from './useColumnsState'; 2 | -------------------------------------------------------------------------------- /server/frontend/src/hooks/useColumnsState.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useColumnsState = (initialState, localStorageKey) => { 4 | const [columnsStateMap, setColumnsStateMap] = useState(); 5 | 6 | const localStorageState = localStorage[localStorageKey]; 7 | console.log('localStorageState', localStorageState); 8 | 9 | useEffect(() => { 10 | if (localStorageState) { 11 | try { 12 | setColumnsStateMap(JSON.parse(localStorageState)); 13 | } catch (e) { 14 | setColumnsStateMap(initialState); 15 | localStorage[localStorageKey] = ''; 16 | } 17 | } else { 18 | setColumnsStateMap(initialState); 19 | } 20 | }, []) 21 | 22 | const setState = (value) => { 23 | console.log('value', value); 24 | localStorage[localStorageKey] = JSON.stringify(value); 25 | setColumnsStateMap(value); 26 | } 27 | 28 | return [ 29 | columnsStateMap, 30 | setState, 31 | ] 32 | } 33 | 34 | export default useColumnsState; -------------------------------------------------------------------------------- /server/frontend/src/models/global.ts: -------------------------------------------------------------------------------- 1 | // 全局共享数据示例 2 | import { useRequest } from '@umijs/max'; 3 | import * as services from '../services/global'; 4 | import { useEffect } from 'react'; 5 | 6 | const useGlobal = () => { 7 | 8 | const getUsers = useRequest(services.getUsers, { 9 | manual: true, 10 | }) 11 | const getModels = useRequest(services.getModels, { 12 | manual: true, 13 | }); 14 | const getJobs = useRequest(services.getJobs, { 15 | manual: true, 16 | }); 17 | const getInvocations = useRequest(services.getInvocations, { 18 | manual: true, 19 | }); 20 | const getWorkers = useRequest(services.getWorks, { 21 | manual: true, 22 | }); 23 | 24 | const getBaseInfo = () => { 25 | getUsers.run(); 26 | getModels.run(); 27 | getJobs.run(); 28 | getInvocations.run(); 29 | getWorkers.run(); 30 | } 31 | 32 | useEffect(() => { 33 | if (localStorage.token) { 34 | getBaseInfo(); 35 | } 36 | }, []) 37 | 38 | return { 39 | getUsers, 40 | getModels, 41 | getJobs, 42 | getInvocations, 43 | getWorkers, 44 | getBaseInfo, 45 | }; 46 | }; 47 | 48 | export default useGlobal; 49 | -------------------------------------------------------------------------------- /server/frontend/src/pages/Access/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageContainer } from '@ant-design/pro-components'; 2 | import { Access, useAccess } from '@umijs/max'; 3 | import { Button } from 'antd'; 4 | 5 | const AccessPage: React.FC = () => { 6 | const access = useAccess(); 7 | return ( 8 | 14 | 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default AccessPage; 22 | -------------------------------------------------------------------------------- /server/frontend/src/pages/Home/components/Overview/index.tsx: -------------------------------------------------------------------------------- 1 | import { ProCard } from '@ant-design/pro-components'; 2 | import { useModel, useRequest } from '@umijs/max'; 3 | import { Space, Typography } from 'antd'; 4 | import * as React from 'react'; 5 | 6 | const Overview = () => { 7 | const state = useModel('global'); 8 | console.log('state', state) 9 | const { getUsers, getModels, getJobs, getInvocations, getWorkers } = state || {}; 10 | 11 | const user = getUsers.data; 12 | const models = getModels.data; 13 | const jobs = getJobs.data; 14 | const invocations = getInvocations.data; 15 | const workers = getWorkers.data; 16 | 17 | 18 | return ( 19 | 20 | 21 |
Name: {user?.name}
22 |
Email: {user?.email}
23 |
Company: {user?.company}
24 |
25 | Verification: {user?.verified_at ? "Email verified" : "Email not verified yet"} 26 |
27 |
28 | 29 |
Models: {(models || []).length}
30 |
Inference Jobs: {(jobs || []).length}
31 |
Invocations: {invocations?.length}
32 |
33 | 34 |
Workers: {workers?.length}
35 |
36 | 37 |
{localStorage.token}
38 |
39 |
40 | ); 41 | } 42 | 43 | export default Overview; -------------------------------------------------------------------------------- /server/frontend/src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageContainer } from '@ant-design/pro-components'; 2 | import { useModel } from '@umijs/max'; 3 | import Overview from './components/Overview'; 4 | import React, { useEffect } from 'react'; 5 | 6 | const HomePage: React.FC = () => { 7 | useEffect(() => { 8 | const link = document.querySelector("link[rel*='icon']") || document.createElement('link'); 9 | link.type = 'image/x-icon'; 10 | link.rel = 'icon'; 11 | link.href = 'https://cdn.clustro.ai/icon.ico'; 12 | document.getElementsByTagName('head')[0].appendChild(link); 13 | }); 14 | 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default HomePage; 23 | -------------------------------------------------------------------------------- /server/frontend/src/pages/InferenceJobs/components/CreateInferenceJobModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { ProForm, ProFormInstance, ProFormSelect, ProFormText } from '@ant-design/pro-components'; 2 | import { useModel, useRequest } from '@umijs/max'; 3 | import { Modal } from 'antd'; 4 | import * as React from 'react'; 5 | import { createInferenceJob } from '../../services'; 6 | 7 | const createInferenceJobModal = ({ 8 | visible, 9 | onClose, 10 | }) => { 11 | const formRef = React.useRef(); 12 | const { getModels } = useModel('global'); 13 | 14 | const { run, loading } = useRequest(createInferenceJob, { 15 | manual: true, 16 | }) 17 | 18 | const handleSubmit = async () => { 19 | const values = await formRef.current?.validateFields(); 20 | await run(values); 21 | onClose(); 22 | getModels.run() 23 | } 24 | 25 | return ( 26 | 34 | null 39 | }} 40 | initialValues={{ 41 | status: 'verified', 42 | 43 | }} 44 | > 45 | 46 | 58 | 59 | 60 | 61 | 73 | 74 | 75 | 76 | 102 | 103 | 104 | 105 | 111 | 112 | 113 | 114 | 120 | 121 | 122 | 123 | 129 | 130 | 131 | 132 | 138 | 139 | 140 | 141 | 142 | ); 143 | } 144 | 145 | export default createInferenceJobModal; -------------------------------------------------------------------------------- /server/frontend/src/pages/InferenceJobs/components/GetInferenceJobModal/index.less: -------------------------------------------------------------------------------- 1 | .modal { 2 | margin-top: 40px; 3 | 4 | :global { 5 | .ant-form-item { 6 | margin: 0; 7 | } 8 | 9 | .ant-form-item .ant-form-item-control-input { 10 | min-height: auto !important; 11 | } 12 | 13 | .ant-descriptions-item-content { 14 | flex: 1; 15 | min-width: 0; 16 | } 17 | } 18 | 19 | .title { 20 | display: flex; 21 | align-items: center; 22 | 23 | .text { 24 | margin-right: 8px; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /server/frontend/src/pages/InferenceJobs/components/GetInferenceJobModal/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState, useEffect, useRef } from 'react'; 3 | import { Modal, Descriptions, Skeleton, Switch, Form, Input } from 'antd'; 4 | import { ProField, ProForm, ProFormText, ProFormSelect } from '@ant-design/pro-components'; 5 | 6 | 7 | import { useRequest, useModel } from '@umijs/max'; 8 | import { getInferenceJob, updateInferenceJobs } from './services'; 9 | 10 | import styles from './index.less'; 11 | 12 | enum Mode { 13 | READ = 'read', 14 | EDIT = 'edit', 15 | } 16 | 17 | const GetInferenceJobModal = ({ 18 | id, 19 | visible, 20 | onClose, 21 | }) => { 22 | const [mode, setMode] = useState(Mode.READ); 23 | const { getInvocations, getUsers } = useModel('global'); 24 | const formRef = useRef(); 25 | 26 | 27 | const { id: userId } = getUsers.data || {}; 28 | 29 | const invocations = getInvocations.data || []; 30 | 31 | const { run, loading, data } = useRequest(() => getInferenceJob({ id }), { 32 | manual: true, 33 | }); 34 | 35 | const updateJobReq = useRequest(params => updateInferenceJobs({ id, ...params }), { 36 | manual: true, 37 | }); 38 | 39 | useEffect(() => { 40 | if (visible && id) { 41 | setMode(Mode.READ) 42 | run(); 43 | } 44 | }, [visible, id]) 45 | 46 | const { name, model_name, updated_at, visibility, active_workers, desired_workers, max_workers, min_workers, description, enabled, model_required_gpu, scaling_type, model_example_input, user_id } = data || {}; 47 | const totalInvocations = (invocations || []).filter(invocation => invocation.inference_job_id === id).length; 48 | 49 | const isOwnerItem = user_id && userId && user_id === userId; 50 | 51 | const handleSwitch = (checked) => { 52 | if (!checked) { 53 | formRef.current?.resetFields(); 54 | } 55 | setMode(checked ? Mode.EDIT : Mode.READ) 56 | } 57 | 58 | const handleConfirm = async () => { 59 | if (mode !== Mode.READ) { 60 | const resp = await formRef.current?.validateFieldsReturnFormatValue?.() 61 | await updateJobReq.run(resp); 62 | } 63 | onListUpdate(); 64 | onClose() 65 | } 66 | 67 | return ( 68 | 77 |
78 | 79 | [] }} 82 | onFinish={async (value) => console.log(value)} 83 | > 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 |
144 |
145 | ) 146 | } 147 | 148 | export default GetInferenceJobModal; -------------------------------------------------------------------------------- /server/frontend/src/pages/InferenceJobs/components/GetInferenceJobModal/services.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http'; 2 | 3 | const { get, put } = http.create('ai'); 4 | 5 | export async function getInferenceJob({ id }) { 6 | return get(`/inference_jobs/${id}`); 7 | } 8 | 9 | 10 | export async function updateInferenceJobs(data) { 11 | const { id } = data; 12 | let params = {} 13 | if (data.min_workers) params['min_workers'] = data.min_workers.toInteger(); 14 | if (data.desired_workers) params['desired_workers'] = data.desired_workers.toInteger(); 15 | if (data.max_workers) params['max_workers'] = data.max_workers.toInteger(); 16 | if (data.job_assignment_type) params['job_assignment_type'] = data.job_assignment_type; 17 | if (data.scaling_type) params['scaling_type'] = data.scaling_type; 18 | 19 | return put('/inference_jobs/'+id, params); 20 | } 21 | -------------------------------------------------------------------------------- /server/frontend/src/pages/InferenceJobs/index.less: -------------------------------------------------------------------------------- 1 | a { 2 | color: rgba(79, 70, 229, 1); 3 | } 4 | 5 | .title { 6 | display: inline-block; 7 | max-width: 100%; 8 | overflow: hidden; 9 | text-overflow: ellipsis; 10 | white-space: nowrap; 11 | } -------------------------------------------------------------------------------- /server/frontend/src/pages/InferenceJobs/services.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http'; 2 | 3 | const { post, put, del } = http.create('ai'); 4 | 5 | export async function createInferenceJob(params) { 6 | if (params.min_workers) params['min_workers'] = parseInt(params.min_workers); 7 | if (params.desired_workers) params['desired_workers'] = parseInt(params.desired_workers); 8 | if (params.max_workers) params['max_workers'] = parseInt(params.max_workers); 9 | return post('/inference_jobs', params); 10 | } 11 | 12 | export async function updateInferenceJobs(id, data) { 13 | let params = {} 14 | if (data.min_workers) params['min_workers'] = parseInt(data.min_workers); 15 | if (data.desired_workers) params['desired_workers'] = parseInt(data.desired_workers); 16 | if (data.max_workers) params['max_workers'] = parseInt(data.max_workers); 17 | if (data.job_assignment_type) params['job_assignment_type'] = data.job_assignment_type; 18 | if (data.scaling_type) params['scaling_type'] = data.scaling_type; 19 | 20 | return put('/inference_jobs/'+id, params); 21 | } 22 | 23 | export async function deleteInferenceJob(id) { 24 | return del('/inference_jobs/'+id); 25 | } -------------------------------------------------------------------------------- /server/frontend/src/pages/Invocations/components/InvocationDetailModal/index.less: -------------------------------------------------------------------------------- 1 | .modal { 2 | margin-top: 40px; 3 | 4 | :global { 5 | .ant-form-item { 6 | margin: 0; 7 | } 8 | 9 | .ant-form-item .ant-form-item-control-input { 10 | min-height: auto !important; 11 | } 12 | 13 | .ant-descriptions-item-content { 14 | flex: 1; 15 | min-width: 0; 16 | } 17 | 18 | .ant-image { 19 | width: 100% !important; 20 | } 21 | } 22 | 23 | .title { 24 | display: flex; 25 | align-items: center; 26 | 27 | .text { 28 | margin-right: 8px; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /server/frontend/src/pages/Invocations/components/InvocationDetailModal/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState, useEffect, useRef } from 'react'; 3 | import { Modal, Descriptions, Skeleton } from 'antd'; 4 | import { ProField, ProForm, ProFormText } from '@ant-design/pro-components'; 5 | 6 | import { useRequest, useModel } from '@umijs/max'; 7 | import { getInvocation } from './services'; 8 | 9 | import styles from './index.less'; 10 | 11 | const InvocationDetailModal = ({ 12 | id, 13 | visible, 14 | onClose, 15 | }) => { 16 | 17 | const { run, loading, data } = useRequest(() => getInvocation({ id }), { 18 | manual: true, 19 | }); 20 | 21 | useEffect(() => { 22 | if (visible && id) { 23 | run(); 24 | } 25 | }, [visible, id]) 26 | 27 | const { status, inference_job_name, input, error, processed_by_worker_name, process_start_time, process_finish_time, result } = data || {}; 28 | 29 | 30 | const resultIsImage = result && result.indexOf('https://cdn.clustro.ai') > -1; 31 | 32 | return ( 33 | 41 |
42 | 43 | [] }} 51 | onFinish={async (value) => console.log(value)} 52 | > 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {resultIsImage ? ( 88 | 89 | ) : ( 90 | 91 | )} 92 | 93 | 94 | 95 | 96 |
97 |
98 | ) 99 | } 100 | 101 | export default InvocationDetailModal; -------------------------------------------------------------------------------- /server/frontend/src/pages/Invocations/components/InvocationDetailModal/services.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http'; 2 | 3 | const { get, put } = http.create('ai'); 4 | 5 | export async function getInvocation({ id }) { 6 | return get(`/invocations/${id}`); 7 | } 8 | -------------------------------------------------------------------------------- /server/frontend/src/pages/Invocations/index.less: -------------------------------------------------------------------------------- 1 | a { 2 | color: rgba(79, 70, 229, 1); 3 | } 4 | 5 | .title { 6 | display: inline-block; 7 | max-width: 100%; 8 | overflow: hidden; 9 | text-overflow: ellipsis; 10 | white-space: nowrap; 11 | } -------------------------------------------------------------------------------- /server/frontend/src/pages/Invocations/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageContainer, ProTable } from '@ant-design/pro-components'; 2 | import { useModel } from '@umijs/max'; 3 | import { Space } from 'antd'; 4 | import { useState, useEffect } from 'react'; 5 | import InvocationDetailModal from './components/InvocationDetailModal'; 6 | import './index.less'; 7 | 8 | const Invocations: React.FC = () => { 9 | const { getInvocations } = useModel('global'); 10 | const [detailId, setDetailId] = useState(); 11 | const [detailModalVisible, setDetailModalVisible] = useState(false); // 详情modal 12 | 13 | const models = getInvocations.data; 14 | 15 | useEffect(() => { 16 | getInvocations.run(); 17 | }, []) 18 | 19 | const handleDetailModalVisible = (id) => { 20 | setDetailId(id); 21 | setDetailModalVisible(true); 22 | } 23 | 24 | const columns = [ 25 | { 26 | dataIndex: 'id', 27 | title: 'ID', 28 | render(id) { 29 | return ( 30 | handleDetailModalVisible(id)}> 31 | {id} 32 | 33 | ) 34 | } 35 | }, 36 | { 37 | dataIndex: 'status', 38 | title: 'Status', 39 | ellipsis: true, 40 | sorter: (a, b) => (a['status'] || '').localeCompare(b['status'] || ''), 41 | sortDirections: ['ascend', 'descend'], 42 | }, 43 | { 44 | dataIndex: 'inference_job_name', 45 | title: 'Job', 46 | ellipsis: true, 47 | sorter: (a, b) => (a['inference_job_name'] || '').localeCompare(b['inference_job_name'] || ''), 48 | sortDirections: ['ascend', 'descend'], 49 | }, 50 | { 51 | dataIndex: 'processed_by_worker_name', 52 | title: 'Worker Name', 53 | ellipsis: true, 54 | }, 55 | { 56 | dataIndex: 'input', 57 | title: 'Input', 58 | ellipsis: true, 59 | }, 60 | { 61 | dataIndex: 'error', 62 | title: 'Error', 63 | ellipsis: true, 64 | }, 65 | { 66 | dataIndex: 'result', 67 | title: 'Result', 68 | ellipsis: true, 69 | }, 70 | { 71 | dataIndex: 'created_at', 72 | title: 'Created time', 73 | sorter: (a, b) => { 74 | const dateA = new Date(a['created_at']); 75 | const dateB = new Date(b['created_at']); 76 | return dateA - dateB; 77 | }, 78 | sortDirections: ['ascend', 'descend'], 79 | ellipsis: true, 80 | }, 81 | { 82 | dataIndex: 'process_start_time', 83 | title: 'Process Start Time', 84 | sorter: (a, b) => { 85 | const dateA = new Date(a['process_start_time']); 86 | const dateB = new Date(b['process_start_time']); 87 | return dateA - dateB; 88 | }, 89 | sortDirections: ['ascend', 'descend'], 90 | ellipsis: true, 91 | }, 92 | { 93 | dataIndex: 'process_finish_time', 94 | title: 'Process Finish Time', 95 | sorter: (a, b) => { 96 | const dateA = new Date(a['process_finish_time']); 97 | const dateB = new Date(b['process_finish_time']); 98 | return dateA - dateB; 99 | }, 100 | sortDirections: ['ascend', 'descend'], 101 | ellipsis: true, 102 | }, 103 | ] 104 | 105 | return ( 106 | 107 | 108 | getInvocations.run(), 127 | setting: { 128 | listsHeight: 400, 129 | }, 130 | }} 131 | pagination={true} 132 | dateFormatter="string" 133 | headerTitle="Invocations" 134 | /> 135 | 136 | setDetailModalVisible(false)} 140 | /> 141 | 142 | ); 143 | }; 144 | 145 | export default Invocations; 146 | -------------------------------------------------------------------------------- /server/frontend/src/pages/Login/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/frontend/src/pages/Login/assets/logo.png -------------------------------------------------------------------------------- /server/frontend/src/pages/Login/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | height: 100%; 4 | 5 | 6 | .left { 7 | flex: 1; 8 | position: absolute; 9 | 10 | .bg { 11 | position: relative; 12 | opacity: 0.3; 13 | --tw-gradient-to: #9089fc var(--tw-gradient-to-position); 14 | --tw-gradient-from: #ff80b5 var(--tw-gradient-from-position); 15 | --tw-gradient-to: rgba(255, 128, 181, 0) var(--tw-gradient-to-position); 16 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 17 | background-image: linear-gradient(to top right, var(--tw-gradient-stops)); 18 | --tw-translate-x: -50%; 19 | // transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 20 | aspect-ratio: 1155/678; 21 | width: 72.1875rem; 22 | } 23 | } 24 | 25 | .content { 26 | flex: 1; 27 | position: relative; 28 | z-index: 2; 29 | 30 | :global { 31 | .ant-pro-form-login-page-logo { 32 | height: 34px; 33 | } 34 | 35 | .ant-pro-form-login-page { 36 | display: flex; 37 | justify-content: center; 38 | height: 100%; 39 | } 40 | 41 | .ant-pro-form-login-page-notice { 42 | display: none; 43 | } 44 | 45 | .ant-pro-form-login-page-container { 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | background-color: transparent; 50 | } 51 | } 52 | } 53 | 54 | .sign-up { 55 | text-align: center; 56 | font-size: 14px; 57 | margin-top: 24px; 58 | display: block; 59 | text-decoration: underline; 60 | cursor: pointer; 61 | } 62 | } -------------------------------------------------------------------------------- /server/frontend/src/pages/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AlipayOutlined, 3 | LockOutlined, 4 | MobileOutlined, 5 | TaobaoOutlined, 6 | UserOutlined, 7 | WeiboOutlined, 8 | } from '@ant-design/icons'; 9 | import { 10 | LoginFormPage, 11 | ProFormCaptcha, 12 | ProFormCheckbox, 13 | ProFormText, 14 | } from '@ant-design/pro-components'; 15 | import { Button, Divider, message, Space, Tabs } from 'antd'; 16 | import type { CSSProperties } from 'react'; 17 | import { useState } from 'react'; 18 | import { useEffect } from 'react'; 19 | import styles from './index.less'; 20 | 21 | import iconLogo from './assets/logo.png'; 22 | import { history, useModel, useRequest, Link } from '@umijs/max'; 23 | import { doLogin } from './services'; 24 | 25 | type LoginType = 'phone' | 'account'; 26 | 27 | const iconStyles: CSSProperties = { 28 | color: 'rgba(0, 0, 0, 0.2)', 29 | fontSize: '18px', 30 | verticalAlign: 'middle', 31 | cursor: 'pointer', 32 | }; 33 | 34 | export default () => { 35 | const { getBaseInfo } = useModel('global'); 36 | 37 | const { run } = useRequest(doLogin, { 38 | manual: true, 39 | }); 40 | 41 | const handleSubmit = async (val: any) => { 42 | const resp = await run(val); 43 | console.log('resp', resp); 44 | localStorage.token = resp["api_key"]; 45 | history.push('/'); 46 | getBaseInfo(); 47 | } 48 | 49 | useEffect(() => { 50 | const link = document.querySelector("link[rel*='icon']") || document.createElement('link'); 51 | link.type = 'image/x-icon'; 52 | link.rel = 'icon'; 53 | link.href = 'https://cdn.clustro.ai/icon.ico'; 54 | document.getElementsByTagName('head')[0].appendChild(link); 55 | }); 56 | 57 | return ( 58 |
65 |
66 |
67 |
68 |
75 |
76 |
77 |
78 | 90 | Sign up for Clustro 91 | 92 | } 93 | onFinish={handleSubmit} 94 | 95 | > 96 | , 101 | }} 102 | placeholder={'Email Address'} 103 | rules={[ 104 | { 105 | required: true, 106 | message: 'Enter Email', 107 | }, 108 | ]} 109 | /> 110 | , 115 | }} 116 | placeholder={'Password'} 117 | rules={[ 118 | { 119 | required: true, 120 | message: 'Enter password', 121 | }, 122 | ]} 123 | /> 124 | 133 | 134 |
135 |
136 |
137 | ); 138 | }; -------------------------------------------------------------------------------- /server/frontend/src/pages/Login/services.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http'; 2 | 3 | const { post } = http.create('ai'); 4 | 5 | export async function doLogin(params: any) { 6 | return post('/login', params); 7 | } 8 | -------------------------------------------------------------------------------- /server/frontend/src/pages/ModelsPage/components/ModelDetailModal/index.less: -------------------------------------------------------------------------------- 1 | .modal { 2 | margin-top: 40px; 3 | 4 | :global { 5 | .ant-form-item { 6 | margin: 0; 7 | } 8 | 9 | .ant-form-item .ant-form-item-control-input { 10 | min-height: auto !important; 11 | } 12 | 13 | .ant-descriptions-item-content { 14 | flex: 1; 15 | min-width: 0; 16 | } 17 | 18 | .ant-image { 19 | width: 100% !important; 20 | } 21 | } 22 | 23 | .title { 24 | display: flex; 25 | align-items: center; 26 | 27 | .text { 28 | margin-right: 8px; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /server/frontend/src/pages/ModelsPage/components/ModelDetailModal/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState, useEffect, useRef } from 'react'; 3 | import { Modal, Descriptions, Skeleton } from 'antd'; 4 | import { ProField, ProForm, ProFormText } from '@ant-design/pro-components'; 5 | 6 | import { useRequest, useModel } from '@umijs/max'; 7 | import { getModel } from './services'; 8 | 9 | import styles from './index.less'; 10 | 11 | const ModelDetailModal = ({ 12 | id, 13 | visible, 14 | onClose, 15 | }) => { 16 | 17 | const { run, loading, data } = useRequest(() => getModel({ id }), { 18 | manual: true, 19 | }); 20 | 21 | useEffect(() => { 22 | if (visible && id) { 23 | run(); 24 | } 25 | }, [visible, id]) 26 | 27 | const { name, updated_at, model_type, required_gpu, artifact, version, invoke_function, runtime_docker_image, description, example_input } = data || {}; 28 | 29 | let inputParseJSON = ''; 30 | let textPraseJson = ''; 31 | 32 | try { 33 | inputParseJSON = example_input ? JSON.parse(example_input) : {}; 34 | } catch (e) { 35 | inputParseJSON = {}; 36 | } 37 | 38 | try { 39 | textPraseJson = inputParseJSON.input ? JSON.parse(inputParseJSON.input) : {}; 40 | } catch (e) { 41 | textPraseJson = inputParseJSON.input; 42 | } 43 | 44 | 45 | return ( 46 | 54 |
55 | 56 | [] }} 59 | onFinish={async (value) => console.log(value)} 60 | > 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | {example_input && ( 103 | 104 | 105 | 106 | )} 107 | 108 | 109 | 110 |
111 |
112 | ) 113 | } 114 | 115 | export default ModelDetailModal; -------------------------------------------------------------------------------- /server/frontend/src/pages/ModelsPage/components/ModelDetailModal/services.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http'; 2 | 3 | const { get, put } = http.create('ai'); 4 | 5 | export async function getModel({ id }) { 6 | return get(`/models/${id}`); 7 | } 8 | -------------------------------------------------------------------------------- /server/frontend/src/pages/ModelsPage/index.less: -------------------------------------------------------------------------------- 1 | a { 2 | color: rgba(79, 70, 229, 1); 3 | } 4 | -------------------------------------------------------------------------------- /server/frontend/src/pages/ModelsPage/services.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http'; 2 | 3 | const { post, put, del } = http.create('ai'); 4 | 5 | export async function createModel(params) { 6 | if (params.required_gpu) params['required_gpu'] = parseInt(params.required_gpu); 7 | return post('/models', params); 8 | } 9 | 10 | export async function createDeafultInferenceJob(params) { 11 | if(!('set_as_model_default' in params)){ 12 | params["set_as_model_default"]=true; 13 | } 14 | return post('/inference_jobs', params); 15 | } 16 | 17 | export async function updateModel(id, data) { 18 | let params = {} 19 | if (data.visibility) params['visibility'] = data.visibility; 20 | if (data.required_gpu) params['required_gpu'] = data.required_gpu - 0; 21 | if (data.version) params['version'] = data.version; 22 | if (data.invoke_function) params['invoke_function'] = data.invoke_function; 23 | if (data.runtime_docker_image) params['runtime_docker_image'] = data.runtime_docker_image; 24 | if (data.description) params['description'] = data.description; 25 | if (data.example_input) params['example_input'] = data.example_input; 26 | 27 | return put('/models/'+id, params); 28 | } 29 | 30 | export async function deleteModel(id) { 31 | return del('/models/'+id); 32 | } -------------------------------------------------------------------------------- /server/frontend/src/pages/Register/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/frontend/src/pages/Register/assets/logo.png -------------------------------------------------------------------------------- /server/frontend/src/pages/Register/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | height: 100%; 4 | 5 | 6 | .left { 7 | flex: 1; 8 | position: absolute; 9 | 10 | .bg { 11 | position: relative; 12 | opacity: 0.3; 13 | --tw-gradient-to: #9089fc var(--tw-gradient-to-position); 14 | --tw-gradient-from: #ff80b5 var(--tw-gradient-from-position); 15 | --tw-gradient-to: rgba(255, 128, 181, 0) var(--tw-gradient-to-position); 16 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 17 | background-image: linear-gradient(to top right, var(--tw-gradient-stops)); 18 | --tw-translate-x: -50%; 19 | // transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 20 | aspect-ratio: 1155/678; 21 | width: 72.1875rem; 22 | } 23 | } 24 | 25 | .content { 26 | flex: 1; 27 | position: relative; 28 | z-index: 2; 29 | 30 | :global { 31 | .ant-pro-form-login-page-logo { 32 | height: 34px; 33 | } 34 | 35 | .ant-pro-form-login-page { 36 | display: flex; 37 | justify-content: center; 38 | height: 100%; 39 | } 40 | 41 | .ant-pro-form-login-page-notice { 42 | display: none; 43 | } 44 | 45 | .ant-pro-form-login-page-container { 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | background-color: transparent; 50 | } 51 | } 52 | } 53 | 54 | .sign-up { 55 | text-align: center; 56 | font-size: 14px; 57 | margin-top: 24px; 58 | display: block; 59 | text-decoration: underline; 60 | cursor: pointer; 61 | } 62 | } -------------------------------------------------------------------------------- /server/frontend/src/pages/Register/services.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http'; 2 | 3 | const { post } = http.create('ai'); 4 | 5 | export async function doSignup(params: any) { 6 | return post('/users', params); 7 | } 8 | 9 | export async function doLogin(params: any) { 10 | return post('/login', params); 11 | } 12 | -------------------------------------------------------------------------------- /server/frontend/src/pages/TestInvocation/services.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http'; 2 | 3 | const { post } = http.create('ai'); 4 | 5 | export async function postJob(params) { 6 | const { jobId, ...rest } = params || {}; 7 | return post(`/inference_jobs/${jobId}/invoke_sync`, rest); 8 | } -------------------------------------------------------------------------------- /server/frontend/src/pages/Workers/services.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http'; 2 | 3 | const { post, put, del } = http.create('ai'); 4 | 5 | export async function createWorker(params) { 6 | return post('/inference_jobs', params); 7 | } 8 | 9 | export async function updateWorker(id, data) { 10 | let params = {} 11 | if (data.working_on) params['working_on'] = data.working_on; 12 | if (data.job_assignment_type) params['job_assignment_type'] = data.job_assignment_type; 13 | if (data.type) params['type'] = data.type; 14 | 15 | return put('/workers/'+id, params); 16 | } 17 | 18 | export async function deleteWorker(id) { 19 | return del('/workers/'+id); 20 | } 21 | -------------------------------------------------------------------------------- /server/frontend/src/services/global.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http'; 2 | 3 | const { get: post } = http.create('ai'); 4 | 5 | export async function getUsers() { 6 | return post('/users'); 7 | } 8 | 9 | export async function getModels() { 10 | return post('/models'); 11 | } 12 | 13 | export async function getJobs() { 14 | return post('/inference_jobs'); 15 | } 16 | 17 | export async function getInvocations() { 18 | return post('/invocations'); 19 | } 20 | 21 | export async function getWorks() { 22 | return post('/workers'); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /server/frontend/src/services/invoke.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/frontend/src/services/invoke.ts -------------------------------------------------------------------------------- /server/frontend/src/utils/format.ts: -------------------------------------------------------------------------------- 1 | // 示例方法,没有实际意义 2 | export function trim(str: string) { 3 | return str.trim(); 4 | } 5 | -------------------------------------------------------------------------------- /server/frontend/src/utils/http/index.js: -------------------------------------------------------------------------------- 1 | import { history } from '@umijs/max'; 2 | import Http from './src'; 3 | 4 | export default new Http({ 5 | contentKey: '', 6 | // servers: process.config.servers, 7 | header({ domain }) { 8 | const headers = {}; 9 | const token = localStorage['token']; 10 | 11 | if (token) { 12 | headers['X-API-Key'] = token; 13 | } 14 | 15 | return headers; 16 | }, 17 | afterAuthorityFailure() { 18 | history.push('/user/login') 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /server/frontend/src/utils/http/src/Request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default class Request { 4 | options; 5 | 6 | interceptors; 7 | 8 | axiosRequest(...args) { 9 | const [method, domain, url, data, options] = args; 10 | 11 | const services = { 12 | ai: process.env.REACT_APP_BACKEND_SERVER_URL 13 | }; 14 | 15 | let config = { 16 | domain, 17 | baseURL: services[domain] || services.qa, 18 | url, 19 | method, 20 | }; 21 | 22 | if (['get'].includes(method)) { 23 | config.params = data; 24 | } else { 25 | config.data = data; 26 | } 27 | 28 | config = { 29 | ...config, 30 | ...this.options, 31 | ...options, 32 | }; 33 | 34 | const instance = axios.create(); 35 | 36 | for (let interceptor of this.interceptors.request) { 37 | instance.interceptors.request.use(interceptor.success); 38 | } 39 | 40 | for (let interceptor of this.interceptors.response) { 41 | // interceptor = applyFn(interceptor, { axiosRequest: this.axiosRequest.bind(this, ...args) }); 42 | 43 | const { success = null, error = null } = interceptor; 44 | 45 | instance.interceptors.response.use(success, error); 46 | } 47 | 48 | return instance.request(config); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server/frontend/src/utils/http/src/index.js: -------------------------------------------------------------------------------- 1 | import Request from './Request'; 2 | import interceptors from './interceptors'; 3 | 4 | export default class Http extends Request { 5 | options; 6 | 7 | interceptors; 8 | 9 | constructor(options) { 10 | super(); 11 | 12 | this.options = { 13 | contentType: 'json', 14 | ...options, 15 | }; 16 | this.interceptors = interceptors; 17 | } 18 | static create(options) { 19 | return new Http(options); 20 | } 21 | 22 | create(domain) { 23 | return { 24 | get: (...args) => this.axiosRequest('get', domain, ...args), 25 | post: (...args) => this.axiosRequest('post', domain, ...args), 26 | del: (...args) => this.axiosRequest('delete', domain, ...args), 27 | put: (...args) => this.axiosRequest('put', domain, ...args), 28 | patch: (...args) => this.axiosRequest('patch', domain, ...args), 29 | head: (...args) => this.axiosRequest('head', domain, ...args), 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/frontend/src/utils/http/src/interceptors.js: -------------------------------------------------------------------------------- 1 | import { message } from 'antd'; 2 | import { history } from '@umijs/max'; 3 | import applyFn from './utils/applyFn'; 4 | 5 | /* 6 | * request query 中间件 7 | */ 8 | export const query = { 9 | success(config) { 10 | // if (baseURL.includes('play')) { 11 | // // 接口的参数不放在url上面的请求列表 12 | // const noQueryList = [ 13 | // 'evaluate-comment', // 订单评论 14 | // 'getConnectPaypalUrl', // 获取paypal绑卡回调地址 15 | // ]; 16 | // const isAddParams = noQueryList.every((item) => !url.includes(item)); 17 | 18 | // if (isAddParams) config.params = data; 19 | // } 20 | 21 | return config; 22 | }, 23 | }; 24 | 25 | /* 26 | * request header 中间件 27 | */ 28 | export const header = { 29 | success(config) { 30 | const { header } = config; 31 | 32 | if (header) { 33 | const headers = applyFn(header, config); 34 | config.headers = { ...headers, ...config.headers }; 35 | } 36 | 37 | return config; 38 | }, 39 | }; 40 | 41 | /* 42 | * request 加密 中间件 43 | */ 44 | export const addKey = { 45 | success(config) { 46 | const { headers, data, domain, url } = config; 47 | const { encrypt, _tk_ } = headers; 48 | 49 | 50 | return config; 51 | }, 52 | }; 53 | 54 | /* 55 | * response data 状态处理中间件 56 | */ 57 | const defaultErrorValidate = (data) => { 58 | return data.status && data.status.toUpperCase() === 'ERROR'; 59 | }; 60 | 61 | export const dataStatus = { 62 | success(response) { 63 | const { data, config } = response; 64 | const { responseDataValidator } = config; 65 | 66 | if ((responseDataValidator || defaultErrorValidate)(data)) { 67 | return Promise.reject(response); 68 | } 69 | 70 | return response; 71 | }, 72 | }; 73 | 74 | /* 75 | * 授权处理中间件,检查是否非法授权 76 | */ 77 | const defaultOptions = { 78 | codes: ['50008', '50012', '50014', '50016', '10050005', '10050006'] 79 | }; 80 | 81 | let hasErrorMessage = false; 82 | const DEFAULT_EXPIRE_MESSAGE = 'Please log in'; 83 | 84 | export const authorityValidator = { 85 | error(response) { 86 | const { data = {}, config } = response; 87 | 88 | /* eslint-disable no-shadow */ 89 | const { authorityValidator, authorityFailureCodes, afterAuthorityFailure } = config || {}; 90 | const authorityOptions = defaultOptions; 91 | 92 | if (authorityValidator) { 93 | return Promise.reject(authorityValidator(data) || data); 94 | } 95 | 96 | if (authorityFailureCodes) { 97 | authorityOptions.codes = authorityFailureCodes; 98 | } 99 | 100 | const { error } = data; 101 | if (error) { 102 | if (!hasErrorMessage) { 103 | // const errorMessage = data.errorMsg || DEFAULT_EXPIRE_MESSAGE; 104 | // message.warning(errorMessage, 2, () => (hasErrorMessage = false)); 105 | 106 | if (afterAuthorityFailure) { 107 | afterAuthorityFailure(data, config); 108 | } 109 | 110 | hasErrorMessage = true; 111 | } 112 | 113 | config.ignoreErrorModal = true; 114 | } 115 | 116 | return Promise.reject(response); 117 | }, 118 | }; 119 | 120 | /* 121 | * responseError 错误处理 122 | */ 123 | const DEFAULT_RES_ERROR = 'System exception, please try again later'; 124 | let hasErrorModal = false; 125 | 126 | const responseError = { 127 | error(response) { 128 | if (response.config && !response.config.ignoreErrorModal && !hasErrorModal) { 129 | message.error( 130 | response?.response.data?.error || DEFAULT_RES_ERROR, 131 | 2, 132 | () => (hasErrorMessage = false), 133 | ); 134 | } 135 | if (response?.response && response?.response?.data.error === 'Invalid API key') { 136 | history.push('/user/login') 137 | } 138 | return Promise.reject(response); 139 | }, 140 | }; 141 | 142 | /* 143 | * response data 内容处理中间件 144 | */ 145 | export const dataContent = { 146 | success(response) { 147 | const key = response.config.contentKey; 148 | 149 | // if (key) { 150 | // return response.data[key]; 151 | // } 152 | 153 | return response; 154 | }, 155 | }; 156 | 157 | export default { 158 | request: [query, header, addKey].reverse(), 159 | response: [dataStatus, authorityValidator, responseError, dataContent], 160 | }; 161 | -------------------------------------------------------------------------------- /server/frontend/src/utils/http/src/utils/applyFn.js: -------------------------------------------------------------------------------- 1 | export default (fn, params) => (typeof fn === 'function' ? fn(params) : fn); 2 | -------------------------------------------------------------------------------- /server/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './src/pages/**/*.tsx', 4 | './src/components/**/*.tsx', 5 | './src/layouts/**/*.tsx', 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /server/frontend/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | .ant-btn-primary { 7 | background-color: rgba(79, 70, 229, 1) !important; 8 | } -------------------------------------------------------------------------------- /server/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./src/.umi/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /server/frontend/typings.d.ts: -------------------------------------------------------------------------------- 1 | import '@umijs/max/typings'; 2 | -------------------------------------------------------------------------------- /server/landingPage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 3000 12 | 13 | CMD [ "npm", "start" ] 14 | -------------------------------------------------------------------------------- /server/landingPage/README.md: -------------------------------------------------------------------------------- 1 | # 官方文档 2 | 3 | 官方文档请查看 [http://doc.ssr-fc.com/](http://doc.ssr-fc.com/) 4 | 5 | ## getting start 6 | 7 | ```bash 8 | $ npm start # 本地开发模式运行,单进程 支持 前端 HMR 前端静态资源走本地 webpack 服务 9 | $ npm run prod # 模拟生产环境运行,多进程,前端资源走静态目录 10 | $ npm run stop # 生产环境停止服务 11 | ``` -------------------------------------------------------------------------------- /server/landingPage/app.js: -------------------------------------------------------------------------------- 1 | // serverless 部署使用 2 | const WebFramework = require('@midwayjs/koa').Framework 3 | const { Bootstrap } = require('@midwayjs/bootstrap') 4 | 5 | module.exports = async () => { 6 | // 加载框架并执行 7 | await Bootstrap.run() 8 | // 获取依赖注入容器 9 | const container = Bootstrap.getApplicationContext() 10 | // 获取 koa framework 11 | const framework = container.get(WebFramework) 12 | // 返回 app 对象 13 | return framework.getApplication() 14 | } 15 | -------------------------------------------------------------------------------- /server/landingPage/bootstrap.js: -------------------------------------------------------------------------------- 1 | const { Bootstrap } = require('@midwayjs/bootstrap') 2 | const { loadConfig } = require('ssr-common-utils') 3 | 4 | const { serverPort } = loadConfig() 5 | 6 | Bootstrap 7 | .configure({ 8 | globalConfig: { 9 | koa: { 10 | port: serverPort 11 | } 12 | } 13 | }) 14 | .run() 15 | -------------------------------------------------------------------------------- /server/landingPage/config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from 'ssr-types' 2 | 3 | const userConfig: UserConfig = { 4 | define: { 5 | base: {} 6 | }, 7 | css: () => { 8 | const tailwindcss = require('tailwindcss') 9 | const autoprefixer = require('autoprefixer') 10 | return { 11 | loaderOptions: { 12 | postcss: { 13 | plugins: [ 14 | tailwindcss, 15 | autoprefixer 16 | ] 17 | } 18 | } 19 | } 20 | } 21 | } 22 | 23 | export { userConfig } 24 | -------------------------------------------------------------------------------- /server/landingPage/f.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: serverless-ssr 3 | provider: 4 | name: aliyun 5 | memorySize: 2048 6 | timeout: 20 7 | initTimeout: 20 8 | runtime: nodejs12 9 | concurrency: 10 10 | region: ap-southeast-1 11 | 12 | custom: # 发布后自动生成测试域名 13 | customDomain: 14 | domainName: auto 15 | 16 | package: 17 | include: 18 | - build 19 | - public 20 | exclude: 21 | - package-lock.json 22 | artifact: code.zip 23 | 24 | deployType: 25 | type: koa ## 部署的应用类型 26 | version: 3.0.0 27 | name: ssr -------------------------------------------------------------------------------- /server/landingPage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "midway-react-ssr", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^5.1.4", 7 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 8 | "@headlessui/react": "^1.7.15", 9 | "@heroicons/react": "^2.0.18", 10 | "@midwayjs/bootstrap": "^3.0.0", 11 | "@midwayjs/core": "^3.0.0", 12 | "@midwayjs/decorator": "^3.0.0", 13 | "@midwayjs/koa": "^3.0.0", 14 | "koa-static-cache": "^5.1.4", 15 | "pm2": "^4.5.4", 16 | "react": "^18.0.0", 17 | "react-dom": "^18.0.0", 18 | "react-router-dom": "^5.1.2", 19 | "ssr-common-utils": "^6.0.0", 20 | "ssr-core": "^6.0.0" 21 | }, 22 | "devDependencies": { 23 | "@midwayjs/mock": "^3.0.0", 24 | "@types/react": "^18.0.0", 25 | "@types/react-dom": "^18.0.0", 26 | "@types/react-router-dom": "^5.1.3", 27 | "eslint-config-standard-react-ts": "^1.0.5", 28 | "ssr": "^6.0.0", 29 | "ssr-plugin-midway": "^6.0.0", 30 | "ssr-plugin-react18": "^6.0.0", 31 | "ssr-types": "^6.0.0", 32 | "tailwindcss": "^3.3.2", 33 | "typescript": "^4.0.0" 34 | }, 35 | "scripts": { 36 | "prod": "ssr build && pm2 start pm2.config.js", 37 | "prod:vite": "ssr build --vite && pm2 start pm2.config.js", 38 | "stop": "pm2 stop pm2.config.js", 39 | "start": "ssr start", 40 | "start:vite": "ssr start --vite", 41 | "build": "ssr build --optimize", 42 | "build:o": "ssr build --optimize", 43 | "build:vite": "ssr build --vite", 44 | "deploy": "ssr build && ssr deploy", 45 | "lint": "eslint . --ext .js,.tsx,.ts --cache", 46 | "lint:fix": "eslint . --ext .js,.tsx,.ts --cache --fix" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/landingPage/pm2.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'ssr-app', 5 | script: 'bootstrap.js', 6 | exec_mode: 'cluster', 7 | max_memory_restart: '500M', 8 | env: { 9 | NODE_ENV: 'production' 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /server/landingPage/src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { MidwayConfig } from '@midwayjs/core' 2 | 3 | export default { 4 | // use for cookie sign key, should change to your own and keep security 5 | keys: '1650192482948_2252' 6 | 7 | } as MidwayConfig 8 | -------------------------------------------------------------------------------- /server/landingPage/src/configuration.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, App } from '@midwayjs/decorator' 2 | import * as koa from '@midwayjs/koa' 3 | import { join } from 'path' 4 | import { initialSSRDevProxy, getCwd } from 'ssr-common-utils' 5 | 6 | const koaStatic = require('koa-static-cache') 7 | const cwd = getCwd() 8 | 9 | @Configuration({ 10 | imports: [ 11 | koa 12 | ], 13 | importConfigs: [join(__dirname, './config')] 14 | }) 15 | export class ContainerLifeCycle { 16 | @App() 17 | app: koa.Application 18 | 19 | async onReady () { 20 | this.app.use(koaStatic(join(cwd, './build'), { maxAge: 864000 })) 21 | this.app.use(koaStatic(join(cwd, './public'), { maxAge: 864000 })) 22 | this.app.use(koaStatic(join(cwd, './build/client'), { maxAge: 864000 })) 23 | 24 | await initialSSRDevProxy(this.app) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/landingPage/src/controller/api.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Controller, Provide, Get } from '@midwayjs/decorator' 2 | import { Context } from '@midwayjs/koa' 3 | import { IApiService, IApiDetailService } from '../interface' 4 | 5 | @Provide() 6 | @Controller('/api') 7 | export class Api { 8 | @Inject() 9 | ctx: Context 10 | 11 | @Inject('ApiService') 12 | service: IApiService 13 | 14 | @Inject('ApiDetailService') 15 | detailService: IApiDetailService 16 | 17 | @Get('/index') 18 | async getIndexData () { 19 | const data = await this.service.index() 20 | return data 21 | } 22 | 23 | @Get('/detail/:id') 24 | async getDetailData () { 25 | const { ctx, detailService } = this 26 | const id = ctx.params.id 27 | const data = await detailService.index(id) 28 | return data 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/landingPage/src/controller/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Controller, Get, Provide, Inject } from '@midwayjs/decorator' 3 | import { Context } from '@midwayjs/koa' 4 | import { render } from 'ssr-core' 5 | import { IApiService, IApiDetailService } from '../interface' 6 | 7 | interface IKoaContext extends Context { 8 | apiService: IApiService 9 | apiDeatilservice: IApiDetailService 10 | } 11 | 12 | @Provide() 13 | @Controller('/') 14 | export class Index { 15 | @Inject() 16 | ctx: IKoaContext 17 | 18 | @Inject('ApiService') 19 | apiService: IApiService 20 | 21 | @Inject('ApiDetailService') 22 | apiDeatilservice: IApiDetailService 23 | 24 | @Get('/') 25 | @Get('/detail/:id') 26 | async handler (): Promise { 27 | // 降级策略参考文档 http://doc.ssr-fc.com/docs/features$csr#%E5%A4%84%E7%90%86%20%E6%B5%81%20%E8%BF%94%E5%9B%9E%E5%BD%A2%E5%BC%8F%E7%9A%84%E9%99%8D%E7%BA%A7 28 | const { ctx } = this 29 | ctx.apiService = this.apiService 30 | ctx.apiDeatilservice = this.apiDeatilservice 31 | const stream = await render(ctx, { 32 | stream: true, 33 | onError: (err) => { 34 | console.log('ssr error', err) 35 | stream.destroy() 36 | render(ctx, { 37 | stream: false, 38 | mode: 'csr' 39 | }).then(csrStr => { 40 | ctx.res.end(csrStr) 41 | }) 42 | return null 43 | } 44 | }) 45 | ctx.body = stream 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/landingPage/src/interface/detail.ts: -------------------------------------------------------------------------------- 1 | import { DetailData } from '~/typings/data' 2 | export interface IApiDetailService { 3 | index: (id: string) => Promise 4 | } 5 | -------------------------------------------------------------------------------- /server/landingPage/src/interface/index.ts: -------------------------------------------------------------------------------- 1 | import { IndexData } from '~/typings/data' 2 | 3 | export interface IApiService { 4 | index: () => Promise 5 | } 6 | 7 | export * from './detail' 8 | -------------------------------------------------------------------------------- /server/landingPage/src/service/detail.ts: -------------------------------------------------------------------------------- 1 | import { Provide } from '@midwayjs/decorator' 2 | import { Ddata } from '~/typings/data' 3 | import mock from '../mock/detail' 4 | 5 | @Provide('ApiDetailService') 6 | export class ApiDetailService { 7 | async index (id): Promise { 8 | return await Promise.resolve(mock.data[id]) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/landingPage/src/service/index.ts: -------------------------------------------------------------------------------- 1 | import { Provide } from '@midwayjs/decorator' 2 | import { IndexData } from '~/typings/data' 3 | import mock from '../mock' 4 | 5 | @Provide('ApiService') 6 | export class ApiIndexService { 7 | async index (): Promise { 8 | return await Promise.resolve(mock) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/landingPage/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const defaultTheme = require('tailwindcss/defaultTheme') 3 | 4 | 5 | module.exports = { 6 | content: ['./web/**/*.{vue,js,ts,jsx,tsx}'], 7 | theme: { 8 | fontFamily: { 9 | sans: ['Inter var', ...defaultTheme.fontFamily.sans], 10 | }, 11 | }, 12 | plugins: [], 13 | } 14 | 15 | -------------------------------------------------------------------------------- /server/landingPage/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /server/landingPage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "target": "ES2018", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "inlineSourceMap":true, 11 | "noImplicitThis": true, 12 | "noUnusedLocals": true, 13 | "esModuleInterop": true, 14 | "stripInternal": true, 15 | "skipLibCheck": true, 16 | "pretty": true, 17 | "declaration": true, 18 | "outDir": "dist", 19 | "jsx": "react-jsx", 20 | "typeRoots": [ "./typings", "./node_modules/@types"], 21 | "paths": { 22 | "~/*": ["./*"], 23 | "@/*": ["./web/*"], 24 | "~src/*": ["./src/*"], 25 | "_build/*": ["./build/*"] 26 | } 27 | }, 28 | "include": [ 29 | "src", 30 | "web" 31 | ] 32 | } 33 | 34 | -------------------------------------------------------------------------------- /server/landingPage/typings/data/detail-index.d.ts: -------------------------------------------------------------------------------- 1 | export interface Ddata { 2 | detailData?: { 3 | data: DetailData[] 4 | } 5 | } 6 | export interface DetailData { 7 | dataNode: RecommendDataNode[] | PlayerDataNode[] | BriefDataNode[] 8 | } 9 | 10 | export interface PlayerDataNode { 11 | data: { 12 | img: string 13 | title: string 14 | } 15 | } 16 | export interface RecommendDataNode { 17 | data: { 18 | heat: string 19 | summary: string 20 | img: string 21 | titleLine: number 22 | summaryType: string 23 | title: string 24 | subtitleType: string 25 | subtitle: string 26 | } 27 | } 28 | 29 | export interface BriefDataNode { 30 | data: { 31 | showName: string 32 | heat: string 33 | heatIcon: string 34 | updateInfo: string 35 | mark: { 36 | type: string 37 | mediaMarkType: string 38 | mediaMarkEnum: { 39 | name: string 40 | } 41 | data: { 42 | text: string 43 | color: string 44 | colorValue?: string 45 | img?: string 46 | } 47 | } 48 | showImg: boolean 49 | subTitleList: subTitle[] 50 | } 51 | } 52 | 53 | export interface subTitle { 54 | subtitle: string 55 | subtitleType: string 56 | } 57 | -------------------------------------------------------------------------------- /server/landingPage/typings/data/foo.d.ts: -------------------------------------------------------------------------------- 1 | // 存放前后端公共类型文件,只能以 d.ts 结尾 2 | // 可通过 tsconfig paths 配置使用 alias 方式引入文件 3 | // import { Foo } from '~/typings/foo' 4 | 5 | export type Foo = string 6 | -------------------------------------------------------------------------------- /server/landingPage/typings/data/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './page-index' 2 | export * from './detail-index' 3 | -------------------------------------------------------------------------------- /server/landingPage/typings/data/page-index.d.ts: -------------------------------------------------------------------------------- 1 | export interface IData { 2 | indexData: IndexData 3 | } 4 | 5 | export interface IndexData { 6 | data: ComponentsArr[] 7 | } 8 | export interface ComponentsArr { 9 | components: ItemMapArr[] 10 | } 11 | 12 | export interface ItemMapArr { 13 | itemMap: ItemMap[] 14 | } 15 | export interface ItemMap { 16 | action: { 17 | type: string 18 | extra: { 19 | value: string 20 | videoId?: string 21 | } 22 | } 23 | mark: { 24 | text: string 25 | } 26 | subtitle?: string 27 | title: string 28 | img: string 29 | summary: string 30 | } -------------------------------------------------------------------------------- /server/landingPage/web/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | import { IWindow } from 'ssr-types' 2 | 3 | declare global { 4 | interface Window extends IWindow {} 5 | const __isBrowser__: Boolean 6 | } 7 | -------------------------------------------------------------------------------- /server/landingPage/web/@types/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.less' 2 | -------------------------------------------------------------------------------- /server/landingPage/web/common.less: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | a { 7 | color: #fff; 8 | text-decoration: none; 9 | } 10 | 11 | ul { 12 | list-style: none; 13 | } 14 | 15 | :global { 16 | .loading { 17 | width: 30px; 18 | height: 30px; 19 | .verticalLevelCenter 20 | } 21 | } 22 | .verticalCenter { 23 | position: absolute; 24 | top: 50%; 25 | transform: translate(0, -50%) 26 | } 27 | .levelCenter { 28 | position: absolute; 29 | left: 50%; 30 | transform: translate(-50%, 0) 31 | } 32 | .verticalLevelCenter { 33 | position: absolute; 34 | left: 50%; 35 | top: 50%; 36 | transform: translate(-50%, -50%) 37 | } 38 | .verticalCenterFlex { 39 | display: flex; 40 | align-items: center; 41 | } 42 | .levelCenterFlex { 43 | display: flex; 44 | justify-content: center; 45 | } 46 | .verticalLevelCenterFlex { 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | } 51 | .overflowEllipsis { 52 | overflow: hidden; 53 | text-overflow:ellipsis; 54 | white-space: nowrap; 55 | } 56 | .pName { 57 | font-size: 15px; 58 | color: #000000; 59 | margin-top: 10px; 60 | padding-left: 9px; 61 | overflow: hidden; 62 | white-space: nowrap; 63 | text-overflow: ellipsis; 64 | height: 25px; 65 | line-height: 25px; 66 | z-index: 99; 67 | } 68 | .pDesc { 69 | padding-left: 9px; 70 | font-size: 13px; 71 | color: #999999; 72 | margin-bottom: 20px; 73 | overflow: hidden; 74 | white-space: nowrap; 75 | text-overflow: ellipsis; 76 | z-index: 99; 77 | } -------------------------------------------------------------------------------- /server/landingPage/web/components/brief/index.module.less: -------------------------------------------------------------------------------- 1 | .brief-info { 2 | padding: 18px 0 0px; 3 | position: relative; 4 | overflow: hidden; 5 | } 6 | 7 | .brief-title { 8 | display: flex; 9 | justify-content: flex-start; 10 | align-items: center; 11 | margin-bottom: 8px; 12 | padding-left: 10px; 13 | span{ 14 | font-size: 11px; 15 | text-align: center; 16 | color: #fff; 17 | font-weight: bold; 18 | background: #FC4273; 19 | padding: 1px 4px; 20 | margin-right: 9px; 21 | border-radius: 3px; 22 | &.icon-GOLDEN{ 23 | background: #EBBA73; 24 | } 25 | } 26 | h1{ 27 | color: #333; 28 | font-size: 18px; 29 | margin-bottom: 0; 30 | } 31 | } 32 | 33 | .brief-score{ 34 | font-size: 12px;; 35 | padding-left: 10px; 36 | color: #666; 37 | .hotVv{ 38 | img{ 39 | width: 14px; 40 | vertical-align: middle; 41 | margin-top: -2px 42 | } 43 | } 44 | 45 | .divide{ 46 | transform: scale(0.6); 47 | margin: 0 5px; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /server/landingPage/web/components/brief/index.tsx: -------------------------------------------------------------------------------- 1 | import { BriefDataNode } from '~/typings/data' 2 | import styles from './index.module.less' 3 | 4 | interface Props { 5 | data: BriefDataNode[] 6 | } 7 | function Brief (props: Props) { 8 | const data = props.data[0].data 9 | return ( 10 |
11 |
12 | {data.mark.data.text} 13 |

{data.showName}

14 |
15 |
16 | { 17 | data.subTitleList.map((item, index) => { 18 | return ( 19 | item.subtitle && ( 20 | 21 | { 22 | item.subtitleType === 'PLAY_VV' 23 | ? 24 | : (index > 0) ? (/) : '' 25 | } 26 | {item.subtitle} 27 | 28 | ) 29 | ) 30 | }) 31 | } 32 |
33 |
34 | ) 35 | } 36 | 37 | export default Brief 38 | -------------------------------------------------------------------------------- /server/landingPage/web/components/layout/App.tsx: -------------------------------------------------------------------------------- 1 | 2 | // 此文件将会在服务端/客户端都将会用到 3 | // 可通过 __isBrowser__ 或者 useEffect 判断当前在 浏览器环境做一些初始化操作 4 | import { LayoutProps } from 'ssr-types' 5 | 6 | export default function App (props: LayoutProps) { 7 | return props.children! 8 | } 9 | -------------------------------------------------------------------------------- /server/landingPage/web/components/layout/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/landingPage/web/components/layout/assets/favicon.ico -------------------------------------------------------------------------------- /server/landingPage/web/components/layout/fetch.ts: -------------------------------------------------------------------------------- 1 | import { ReactMidwayKoaFetch } from 'ssr-types' 2 | 3 | const fetch: ReactMidwayKoaFetch = async ({ ctx, routerProps }) => { 4 | 5 | } 6 | 7 | export default fetch 8 | -------------------------------------------------------------------------------- /server/landingPage/web/components/layout/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /server/landingPage/web/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { LayoutProps } from 'ssr-types' 3 | import App from './App' 4 | import './global.css'; 5 | 6 | const Layout = (props: LayoutProps) => { 7 | // 注:Layout 只会在服务端被渲染,不要在此运行客户端有关逻辑 8 | const { injectState } = props 9 | const { injectCss, injectScript } = props.staticList! 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | Clustro AI 18 | 19 | 20 | { injectCss } 21 | 22 | 23 |
24 | { injectState } 25 | { injectScript } 26 | 27 | 28 | ) 29 | } 30 | 31 | export default Layout 32 | -------------------------------------------------------------------------------- /server/landingPage/web/components/player/index.module.less: -------------------------------------------------------------------------------- 1 | @import '../../common.less'; 2 | 3 | .playerContainer { 4 | position: relative; 5 | width: 100%; 6 | height: 2.1rem; 7 | } 8 | .video { 9 | .playerContainer 10 | } 11 | .title { 12 | position: absolute; 13 | top: 10px; 14 | left: 10px; 15 | color: #fff; 16 | font-size: 15px; 17 | z-index: 100; 18 | } 19 | .ico { 20 | .verticalLevelCenter; 21 | width: 45px; 22 | height: 45px; 23 | z-index: 100; 24 | } 25 | .layer { 26 | position: absolute; 27 | width: 100%; 28 | height: 100%; 29 | z-index: 99; 30 | background-color: #000; 31 | opacity: 0.5; 32 | } -------------------------------------------------------------------------------- /server/landingPage/web/components/player/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { PlayerDataNode } from '~/typings/data' 3 | import styles from './index.module.less' 4 | 5 | interface Props { 6 | data: PlayerDataNode[] 7 | } 8 | function Player (props: Props) { 9 | const [play, setPlay] = useState(false) 10 | const data = props.data[0].data 11 | return ( 12 |
13 | { 14 | play ?
15 | 18 |
:
21 |
{data.title}
22 | setPlay(true)} /> 23 |
24 |
25 | } 26 |
27 | ) 28 | } 29 | 30 | export default Player 31 | -------------------------------------------------------------------------------- /server/landingPage/web/components/recommend/index.module.less: -------------------------------------------------------------------------------- 1 | .title { 2 | display: flex; 3 | height: 50px; 4 | align-items: center; 5 | font-size: 16px; 6 | color: #000; 7 | margin-left: 0.09rem; 8 | font-weight: 700; 9 | } 10 | .reContainer { 11 | display: flex; 12 | flex-wrap: wrap; 13 | justify-content: space-between; 14 | margin: 0 auto; 15 | width: 3.5rem; 16 | } 17 | .reContent { 18 | display: flex; 19 | width: 33%; 20 | flex-direction: column; 21 | white-space: nowrap; 22 | text-overflow: ellipsis; 23 | // padding-right: 0.03rem; 24 | img { 25 | width: 100%; 26 | height: 1.72rem; 27 | } 28 | .vTitle { 29 | color: #444; 30 | font-size: 15px; 31 | font-weight: 400; 32 | } 33 | .subTitle { 34 | font-size: 13px; 35 | color: #999; 36 | white-space: nowrap; 37 | text-overflow: ellipsis; 38 | overflow-x: hidden; 39 | } 40 | } -------------------------------------------------------------------------------- /server/landingPage/web/components/recommend/index.tsx: -------------------------------------------------------------------------------- 1 | import { RecommendDataNode } from '~/typings/data' 2 | import styles from './index.module.less' 3 | 4 | interface Props { 5 | data: RecommendDataNode[] 6 | } 7 | 8 | function Recommend (props: Props) { 9 | const data = props.data 10 | return ( 11 |
12 |
13 | 为你推荐 14 |
15 |
16 | { 17 | data.map(item => ( 18 |
19 | 20 |
21 | {item.data.title} 22 |
23 |
24 | {item.data.subtitle} 25 |
26 |
27 | )) 28 | } 29 |
30 |
31 | ) 32 | } 33 | 34 | export default Recommend 35 | -------------------------------------------------------------------------------- /server/landingPage/web/components/rectangle/index.module.less: -------------------------------------------------------------------------------- 1 | @import '../../common.less'; 2 | 3 | .pbbContainer { 4 | display: flex; 5 | flex-flow: row wrap; 6 | justify-content: space-between; 7 | overflow: hidden; 8 | box-sizing: border-box; 9 | 10 | .pbbItemContainer{ 11 | position: relative; 12 | display: block; 13 | width: 50%; 14 | padding-right: 2px; 15 | box-sizing: border-box; 16 | overflow: hidden; 17 | &:nth-child(2n) { 18 | margin-right: -3px; 19 | } 20 | } 21 | 22 | .defaultItemBg { 23 | position: relative; 24 | width: 100%; 25 | height: 1.04rem; 26 | background-repeat: no-repeat; 27 | background-position: '0 0'; 28 | background-size: 'cover'; 29 | } 30 | 31 | } 32 | .pbbDescContainer { 33 | width: 100%; 34 | display: flex; 35 | flex-direction: column; 36 | } 37 | .pbbImg { 38 | width: 100%; 39 | height: 1.04rem; 40 | } 41 | .pbbName { 42 | width: 1.7rem; 43 | } -------------------------------------------------------------------------------- /server/landingPage/web/components/rectangle/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.less' 2 | 3 | function Rectangle (props) { 4 | const data = props.data[0] 5 | return ( 6 |
7 | { 8 | data.itemMap.map(val => { 9 | const imgUrl = val.img 10 | return ( 11 |
props.history.push('/detail/cbba934b14f747049187')}> 12 |
13 |
16 |
17 | { val.title } 18 |
19 |
20 | {val.subtitle} 21 |
22 |
23 |
24 | ) 25 | }) 26 | } 27 |
28 | ) 29 | } 30 | 31 | export default Rectangle 32 | -------------------------------------------------------------------------------- /server/landingPage/web/components/search/index.module.less: -------------------------------------------------------------------------------- 1 | .searchContainer { 2 | width: 100%; 3 | height: 50px; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | .input{ 9 | width: 80%; 10 | height: 30px; 11 | border: 0 solid transparent; 12 | border-radius: 100px; 13 | font-family: Microsoft YaHei,SimHei,helvetica,arial,verdana,tahoma,sans-serif; 14 | font-size: 14px; 15 | background: rgba(0,0,0,.06); 16 | text-indent: 15px; 17 | outline: none; 18 | margin: 20px 0px; 19 | } 20 | .searchImg { 21 | width: 20px; 22 | margin-left: -30px; 23 | } -------------------------------------------------------------------------------- /server/landingPage/web/components/search/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { IContext } from 'ssr-types' 3 | import { useStoreContext } from 'ssr-common-utils' 4 | import { IData } from '~/typings/data' 5 | import styles from './index.module.less' 6 | 7 | interface SearchState extends IData { 8 | search?: { 9 | text: string 10 | } 11 | } 12 | // 参考本组件学习如何使用 useContext 来跨组件/页面共享状态 13 | function Search () { 14 | const { state, dispatch } = useContext>(useStoreContext()) 15 | const handleChange = (e: React.ChangeEvent) => { 16 | dispatch?.({ 17 | type: 'updateContext', 18 | payload: { 19 | search: { 20 | text: e.target.value 21 | } 22 | } 23 | }) 24 | } 25 | const toSearch = () => { 26 | const searchVal = state?.search?.text ?? '' 27 | location.href = `https://search.youku.com/search_video?keyword=${searchVal}` 28 | } 29 | return ( 30 |
31 | {/* 这里需要给 value 一个兜底的状态 否则 context 改变 首次 render 的 text 值为 undefined 会导致 input 组件 unmount */} 32 | {/* ref: https://stackoverflow.com/questions/47012169/a-component-is-changing-an-uncontrolled-input-of-type-text-to-be-controlled-erro/47012342 */} 33 | 34 | 35 |
36 | ) 37 | } 38 | 39 | export default Search 40 | -------------------------------------------------------------------------------- /server/landingPage/web/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/landingPage/web/images/icon.png -------------------------------------------------------------------------------- /server/landingPage/web/pages/detail/fetch.ts: -------------------------------------------------------------------------------- 1 | import { ReactMidwayKoaFetch } from 'ssr-types' 2 | import { Ddata } from '~/typings/data' 3 | 4 | const fetch: ReactMidwayKoaFetch<{ 5 | apiDeatilservice: { 6 | index: (id: string) => Promise 7 | } 8 | }, {id: string}> = async ({ ctx, routerProps }) => { 9 | // 阅读文档获得更多信息 http://doc.ssr-fc.com/docs/features$fetch#%E5%88%A4%E6%96%AD%E5%BD%93%E5%89%8D%E7%8E%AF%E5%A2%83 10 | const data = __isBrowser__ ? await (await window.fetch(`/api/detail/${routerProps!.match.params.id}`)).json() : await ctx!.apiDeatilservice.index(ctx!.params.id) 11 | return { 12 | // 建议根据模块给数据加上 namespace防止数据覆盖 13 | detailData: data 14 | } 15 | } 16 | export default fetch 17 | -------------------------------------------------------------------------------- /server/landingPage/web/pages/detail/render$id.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { IContext, SProps } from 'ssr-types' 3 | import Player from '@/components/player' 4 | import Brief from '@/components/brief' 5 | import Recommend from '@/components/recommend' 6 | import Search from '@/components/search' 7 | import { Ddata, RecommendDataNode, PlayerDataNode, BriefDataNode } from '~/typings/data' 8 | import { useStoreContext } from 'ssr-common-utils' 9 | 10 | export default function Detail (props: SProps) { 11 | const { state, dispatch } = useContext>(useStoreContext()) 12 | return ( 13 |
14 | 15 | { 16 | state?.detailData?.data[0].dataNode ?
17 | 18 | 19 | 20 |
: 21 | } 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/CoreSection/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const CoreSection = () => { 4 | return ( 5 |
6 |
7 |

Mixed compute resources

8 |

Community commute providers for cost efficient and energy benefits. Clustro managed hosts for inference stability and security.

9 | 10 | Guidebook 11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | export default CoreSection; -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/DemoSection/2_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/landingPage/web/pages/index/components/DemoSection/2_1.gif -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/DemoSection/2_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/landingPage/web/pages/index/components/DemoSection/2_2.gif -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/DemoSection/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import icon1 from './2_1.gif'; 4 | import icon2 from './2_2.gif'; 5 | 6 | const DemoSection = () => { 7 | return ( 8 |
9 |
10 |
11 |

12 | Try our DEMO 13 |

14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 | 28 | Signup as a user 29 | 30 | 31 |

32 | Register to try out pre-deployed models for yourself. 33 |

34 |
35 |
36 |
37 | ); 38 | } 39 | 40 | export default DemoSection; -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/EnvironmentSection/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/landingPage/web/pages/index/components/EnvironmentSection/icon.jpg -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/EnvironmentSection/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import icon from './icon.jpg'; 4 | 5 | const EnvironmentSection = () => { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 |

19 | Caring about our environment 20 |

21 |

22 | Slash the initial capital outlay by 90%, significantly cutting down on material wastage. 23 |

24 |

25 | Address humanity's urgent challenges by utilizing dormant resources effectively. 26 |

27 |

28 | All earnings are funneled into the establishment of green, renewable energy facilities. 29 |

30 |
31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | export default EnvironmentSection; -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/ExplainSection/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/landingPage/web/pages/index/components/ExplainSection/icon.png -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/ExplainSection/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import icon from './icon.png'; 4 | 5 | const ExplainSection = () => { 6 | 7 | return ( 8 |
9 |
10 |

11 | Deploy AI inference on any device 12 |

13 |

14 | Clustro orchestrates a dynamic exchange between users needing computational power and those offering their devices to the network. Discarding the need for centralized facilities and staff, Clustro acts as the Uber and Airbnb of AI cloud computing, directly coupling providers and users, ensuring extraordinarily low costs. 15 |

16 |
17 | 18 |
19 |
20 |
21 | ) 22 | return ( 23 |
24 | 25 |
26 |
27 |
28 |

29 | Deploy AI inference on any device 30 |

31 |

32 | Clustro orchestrates a dynamic exchange between users needing computational power and those offering their devices to the network. 33 |

34 | 35 | 36 |
37 |
38 |
39 |
40 | ); 41 | } 42 | 43 | export default ExplainSection; -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import icon from './logo.png'; 4 | 5 | const Footer = () => { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 | 13 |
14 | Clustro AI 15 |
16 |
17 |

18 | Ship AI faster. 19 |

20 |
21 | Twitter 22 | Discord 23 | GitHub 24 |
25 | 26 |
27 | Contact Us: admin@clustro.ai 28 |
29 |
30 | 31 |
32 |
33 |

34 | © 2023 Clustro AI, Inc. All rights reserved. 35 |

36 |
37 |
38 |
39 | ); 40 | } 41 | 42 | export default Footer; -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/Footer/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/landingPage/web/pages/index/components/Footer/logo.png -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Dialog } from '@headlessui/react' 3 | import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline' 4 | import icon from './logo.png'; 5 | 6 | const Header = () => { 7 | return ( 8 |
9 | 37 |
38 | 39 | ); 40 | } 41 | 42 | export default Header; -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/Header/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/landingPage/web/pages/index/components/Header/logo.png -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/Header/logo.png.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteNova-Official/ByteNova/208c2f30337478d0c097deea914a13c46a92664e/server/landingPage/web/pages/index/components/Header/logo.png.zip -------------------------------------------------------------------------------- /server/landingPage/web/pages/index/components/LandingSection/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline' 3 | 4 | const LandingSection = () => { 5 | return ( 6 |
7 |