Switch REST API to flask-restx and added APIv2 (#9)

* Refactored code to be able to handle multiple Hordes
* Switched to flask-restx for API
* Refactored API to be able to handle multiple API versions
* Added API documentation
* Added limiter per endpoint
This commit is contained in:
Divided by Zer0 authored and GitHub committed 2022-09-27 16:08:03 +02:00
1 parent db85a390ce
commit e9288ec5cd
33 files changed
+2302 -1026

No files matched your search

+2 -1
View File
@@ -131,4 +131,5 @@ dmypy.json
db/*
cliRequestsData.py
*horde_generation*.png
*horde_generation*.png
test_commands.txt
+5 -14
View File
@@ -22,7 +22,6 @@ The point of registering is to track your usage and your contributions. The more
# Generating Prompts
### GUI
We provide [a client interface](https://dbzer0.itch.io/stable-horde-client) requiring no installation and no technical expertise
@@ -47,28 +46,20 @@ You can make a copy of `cliRequestData_template.py` into `cliRequestData.py` and
## REST API
[Full Documentation](https://stablehorde.net/api/v1)
![](api_screenshot.png)
You can also use the REST API directly. Be aware that this will return a base64 encoded image, so it will flood your output. This is not recommended unless you know what you're doing!
```
curl -H "Content-Type: application/json" -d '{"prompt":"A horde of stable robots", "params":{"n":1, "width": 256, "height": 256}, "api_key":"0000000000"}' https://stablehorde.net/api/latest/generate/sync
curl -H "Content-Type: application/json" -d '{"prompt":"A horde of stable robots", "params":{"n":1, "width": 256, "height": 256}, "api_key":"0000000000"}' https://stablehorde.net/api/v1/generate/sync
```
The "params" dictionary is the same as use by the Stable API Webui. Documentation will be forthcoming.
Pass an API Keyin order to track your usage.
### Other API endpoints
Full configuration on the API page coming soon
API root: `/api/latest`
* `/status/performance`: Performance of the Stable Horde as a whole
* `/servers`: All active servers and their stats
* `/servers/<id>`: Specific Server.
* `/users`: All active users and their stats
* `/users/<id>`: Specific User.
## Specifying servers
You can optionally specify only specific servers to generate for you. Grab one or more server IDs from `/servers` and then send it with your payload as a list in the "servers" arg. Your generation will only be fulfiled by servers with the specified IDs
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

+67
View File
@@ -0,0 +1,67 @@
from .logger import logger, set_logger_verbosity, quiesce_logger
from .argparser import args
set_logger_verbosity(args.verbosity)
quiesce_logger(args.quiet)
from .classes.maintenance import Maintenance
maintenance = Maintenance()
from .limiter import limiter
from flask import Flask, render_template, redirect, url_for, request, Blueprint
from .flask import HORDE
from . import routes
from .apis import apiv1, apiv2
from flask_dance.contrib.google import make_google_blueprint, google
from flask_dance.contrib.discord import make_discord_blueprint, discord
from flask_dance.contrib.github import make_github_blueprint, github
import os
HORDE.register_blueprint(apiv1)
HORDE.register_blueprint(apiv2)
@HORDE.before_request
def limit_remote_addr():
logger.debug(request.remote_addr)
# if not allow_direct_connections and request.remote_addr != '127.0.0.1':
# error_msg = get_error(ServerErrors.NO_PROXY)
# abort(403, error_msg)
@HORDE.after_request
def after_request(response):
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS, PUT, DELETE"
response.headers["Access-Control-Allow-Headers"] = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, apikey"
return response
google_client_id = os.getenv("GOOGLE_CLIENT_ID")
google_client_secret = os.getenv("GLOOGLE_CLIENT_SECRET")
discord_client_id = os.getenv("DISCORD_CLIENT_ID")
discord_client_secret = os.getenv("DISCORD_CLIENT_SECRET")
github_client_id = os.getenv("GITHUB_CLIENT_ID")
github_client_secret = os.getenv("GITHUB_CLIENT_SECRET")
HORDE.secret_key = os.getenv("secret_key")
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
google_blueprint = make_google_blueprint(
client_id = google_client_id,
client_secret = google_client_secret,
reprompt_consent = True,
redirect_url='/register',
scope = ["email"],
)
HORDE.register_blueprint(google_blueprint,url_prefix="/google")
discord_blueprint = make_discord_blueprint(
client_id = discord_client_id,
client_secret = discord_client_secret,
scope = ["identify"],
redirect_url='/finish_dance',
)
HORDE.register_blueprint(discord_blueprint,url_prefix="/discord")
github_blueprint = make_github_blueprint(
client_id = github_client_id,
client_secret = github_client_secret,
scope = ["identify"],
redirect_url='/finish_dance',
)
HORDE.register_blueprint(github_blueprint,url_prefix="/github")
+9
View File
@@ -0,0 +1,9 @@
from .. import args
from importlib import import_module
ModelsV2 = import_module(name=f'horde.apis.models.{args.horde}_v2').Models
ParsersV2 = import_module(name=f'horde.apis.models.{args.horde}_v2').Parsers
from .apiv2 import blueprint as apiv2
from .apiv1 import blueprint as apiv1
+22
View File
@@ -0,0 +1,22 @@
from flask import Blueprint
from flask_restx import Api
from .. import args
from importlib import import_module
from .v1.v1 import api as v1
ModelsV2 = import_module(name=f'horde.apis.models.{args.horde}_v2').Models
v2 = import_module(name=f'.{args.horde}', package=f'horde.apis.v2').api
blueprint = Blueprint('apiv1', __name__, url_prefix='/api')
api = Api(blueprint,
version='1.0',
title='Stable Horde',
description='The API documentation for the Stable Horde',
contact_email="mail@dbzer0.com",
default="v1",
default_label="Obsolete Version",
ordered=True,
)
api.add_namespace(v2)
api.add_namespace(v1)
+21
View File
@@ -0,0 +1,21 @@
from flask import Blueprint
from flask_restx import Api
from .. import args
from importlib import import_module
from .v1.v1 import api as v1
v2 = import_module(name=f'.{args.horde}', package=f'horde.apis.v2').api
blueprint = Blueprint('apiv2', __name__, url_prefix='/api')
api = Api(blueprint,
version='2.0',
title='Stable Horde',
description='The API documentation for the Stable Horde',
contact_email="mail@dbzer0.com",
default="v2",
default_label="Latest Version",
ordered=True,
)
api.add_namespace(v2)
api.add_namespace(v1)
+106
View File
@@ -0,0 +1,106 @@
from werkzeug import exceptions as wze
from ..logger import logger
class MissingPrompt(wze.BadRequest):
def __init__(self, username):
self.specific = "You cannot specify an empty prompt."
self.log = f"User '{username}' sent an empty prompt. Aborting!"
class KudosValidationError(wze.BadRequest):
def __init__(self, username, error_message):
self.specific = error_message
self.log = f"User '{username}' Failed to transfer Kudos."
class NoValidActions(wze.BadRequest):
def __init__(self, error_message):
self.specific = error_message
self.log = None
class InvalidSize(wze.BadRequest):
def __init__(self, username):
self.specific = "Invalid size. The image dimentions have to be multiples of 64."
self.log = f"User '{username}' sent an invalid size. Aborting!"
class TooManySteps(wze.BadRequest):
def __init__(self, username, steps):
self.specific = "Too many sampling steps. To allow resources for everyone, we allow only up to 100 steps."
self.log = f"User '{username}' sent too many steps ({steps}). Aborting!"
class InvalidAPIKey(wze.Unauthorized):
def __init__(self, subject):
self.specific = "No user matching sent API Key. Have you remembered to register at https://stablehorde.net/register ?"
self.log = f"Invalid API Key sent for {subject}"
class WrongCredentials(wze.Forbidden):
def __init__(self, username, worker):
self.specific = "You cannot specify an empty prompt."
self.log = f"User '{username}' sent wrong credentials for utilizing worker {worker}"
class NotAdmin(wze.Forbidden):
def __init__(self, username, endpoint):
self.specific = "You're not an admin. Sod off!"
self.log = f"Non-admin user '{username}' tried to use admin endpoint: '{endpoint}. Aborting!"
class NotOwner(wze.Forbidden):
def __init__(self, username, worker_name):
self.specific = "You're not an admin. Sod off!"
self.log = f"User '{username}'' tried to modify worker they do not own '{worker_name}'. Aborting!"
class WorkerMaintenance(wze.Forbidden):
def __init__(self):
self.specific = "This worker has been put into maintenance by its owner"
self.log = None
class InvalidProcGen(wze.NotFound):
def __init__(self, gen_id):
self.specific = f"Processing Generation with ID {gen_id} does not exist."
self.log = f"Worker attempted to provide generation for {gen_id} but it did not exist"
class RequestNotFound(wze.NotFound):
def __init__(self, wp_id):
self.specific = f"Request with ID '{wp_id}' not found."
self.log = f"Status of WP with ID '{wp_id}' does not exist"
class WorkerNotFound(wze.NotFound):
def __init__(self, worker_id):
self.specific = f"Worker with ID '{worker_id}' not found."
self.log = f"Attempted to retrieve worker with non-existent ID '{worker_id}'"
class UserNotFound(wze.NotFound):
def __init__(self, user_id):
self.specific = f"User with ID '{user_id}' not found."
self.log = f"Attempted to retrieve user with non-existent ID '{user_id}'"
class DuplicateGen(wze.NotFound):
def __init__(self, worker, gen_id):
self.specific = f"Processing Generation with ID {gen_id} already submitted."
self.log = f"Worker '{worker}' attempted to provide duplicate generation for {gen_id}"
class RequestExpired(wze.Gone):
def __init__(self, username):
self.specific = f"Prompt Request Expired"
self.log = f"Request from '{username}' took too long to complete and has been cancelled."
class TooManyPrompts(wze.TooManyRequests):
def __init__(self, username, count):
self.specific = f"Parallel requests exceeded user limit ({count}). Please try again later or request to increase your concurrency."
self.log = f"User '{username}' has already requested too many parallel requests ({count}). Aborting!"
class NoValidWorkers(wze.ServiceUnavailable):
retry_after = 600
def __init__(self, username):
self.specific = f"No active worker found to fulfill this request. Please Try again later..."
self.log = f"No active worker found to match the request from '{username}'. Aborting!"
class MaintenanceMode(wze.ServiceUnavailable):
retry_after = 60
def __init__(self, endpoint):
self.specific = f"Horde has enterred maintenance mode. Please try again later."
self.log = f"Rejecting endpoint '{endpoint}' because horde in maintenance mode."
def handle_bad_requests(error):
'''Namespace error handler'''
if error.log:
logger.warning(error.log)
return({'message': error.specific}, error.code)
+1
View File
@@ -0,0 +1 @@
from .stable_v2 import Models
+71
View File
@@ -0,0 +1,71 @@
from flask_restx import fields, reqparse
from . import v2
class Parsers(v2.Parsers):
def __init__(self):
self.job_pop_parser.add_argument("max_pixels", type=int, required=False, default=512, help="The maximum amount of pixels this worker can generate", location="json")
self.job_submit_parser.add_argument("seed", type=str, required=True, default=[], help="The seed of the generation", location="json")
class Models(v2.Models):
def __init__(self,api):
super().__init__(api)
self.response_model_generation_result = api.inherit('GenerationStable', self.response_model_generation_result, {
'img': fields.String(title="Generated Image", description="The generated image as a Base64-encoded .webp file"),
'seed': fields.String(title="Generation Seed", description="The seed which generated this image"),
})
self.response_model_wp_status_full = api.inherit('RequestStatusStable', self.response_model_wp_status_lite, {
'generations': fields.List(fields.Nested(self.response_model_generation_result)),
})
self.response_model_generation_payload = api.model('ModelPayload', {
'prompt': fields.String(description="The prompt which will be sent to Stable Diffusion to generate an image"),
'ddim_steps': fields.Integer(example=50),
'sampler_name': fields.String(enum=["k_lms", "k_heun", "k_euler", "k_euler_a", "k_dpm_2", "k_dpm_2_a", "DDIM", "PLMS"]),
'toggles': fields.List(fields.Integer,example=[1,4], description="Special Toggles used in the SD Webui. To be documented."),
'realesrgan_model_name': fields.String,
'ddim_eta': fields.Float,
'n_iter': fields.Integer(example=1, description="The amount of images to generate"),
'batch_size': fields.Integer(example=1),
'cfg_scale': fields.Float(example=5.0),
'seed': fields.String(description="The seed to use to generete this request"),
'height': fields.Integer(example=512,description="The height of the image to generate"),
'width': fields.Integer(example=512,description="The width of the image to generate"),
'fp': fields.Integer(example=512),
'variant_amount': fields.Float,
'variant_seed': fields.Integer
})
self.response_model_generations_skipped = api.inherit('NoValidRequestFoundStable', self.response_model_generations_skipped, {
'max_pixels': fields.Integer(example=0,description="How many waiting requests were skipped because they demanded a higher size than this worker provides"),
})
self.response_model_job_pop = api.model('GenerationPayload', {
'payload': fields.Nested(self.response_model_generation_payload,skip_none=True),
'id': fields.String(description="The UUID for this image generation"),
'skipped': fields.Nested(self.response_model_generations_skipped,skip_none=True)
})
self.response_model_worker_details = api.inherit('WorkerDetailsStable', self.response_model_worker_details, {
"max_pixels": fields.Integer(example=262144,description="The maximum pixels in resolution this workr can generate"),
"megapixelsteps_generated": fields.Float(description="How many megapixelsteps this worker has generated until now"),
})
self.response_model_contrib_details = api.inherit('ContributionsDetailsStable', self.response_model_contrib_details, {
"megapixelsteps": fields.Float(description="How many megapixelsteps this user has generated"),
})
self.response_model_use_details = api.inherit('UsageDetailsStable', self.response_model_use_details, {
"megapixelsteps": fields.Float(description="How many megapixelsteps this user has requested"),
})
self.response_model_user_details = api.model('UserDetails', {
"username": fields.String(description="The user's unique Username. It is a combination of their chosen alias plus their ID."),
"id": fields.Integer(description="The user unique ID. It is always an integer."),
"kudos": fields.Float(description="The amount of Kudos this user has. Can be negative. The amount of Kudos determines the priority when requesting image generations."),
"kudos_details": fields.Nested(self.response_model_user_kudos_details),
"usage": fields.Nested(self.response_model_use_details),
"contributions": fields.Nested(self.response_model_contrib_details),
"concurrency": fields.Integer(description="How many concurrent image generations this user may request."),
})
self.response_model_horde_performance = api.inherit('HordePerformanceStable', self.response_model_horde_performance, {
"queued_requests": fields.Integer(description="The amount of waiting and processing requests currently in this Horde"),
"queued_megapixelsteps": fields.Float(description="The amount of megapixelsteps in waiting and processing requests currently in this Horde"),
"past_minute_megapixelsteps": fields.Float(description="How many megapixelsteps this Horde generated in the last minute"),
"worker_count": fields.Integer(description="How many workers are actively processing image generations in this Horde in the past 5 minutes"),
})
+137
View File
@@ -0,0 +1,137 @@
from flask_restx import fields, reqparse
class Parsers:
generate_parser = reqparse.RequestParser()
generate_parser.add_argument("apikey", type=str, required=True, help="The API Key corresponding to a registered user", location='headers')
generate_parser.add_argument("prompt", type=str, required=True, help="The prompt to generate from", location="json")
generate_parser.add_argument("params", type=dict, required=False, default={}, help="Extra generate params to send to the worker", location="json")
generate_parser.add_argument("workers", type=str, action='append', required=False, default=[], help="If specified, only the worker with this ID will be able to generate this prompt", location="json")
# generate_parser.add_argument("models", type=str, action='append', required=False, default=[], help="Models", location="json")
# The parser for RequestPop
job_pop_parser = reqparse.RequestParser()
job_pop_parser.add_argument("apikey", type=str, required=True, help="The API Key corresponding to a registered user", location='headers')
job_pop_parser.add_argument("name", type=str, required=True, help="The worker's unique name, to track contributions", location="json")
job_pop_parser.add_argument("priority_usernames", type=str, action='append', required=False, default=[], help="The usernames which get priority use on this worker", location="json")
job_submit_parser = reqparse.RequestParser()
job_submit_parser.add_argument("apikey", type=str, required=True, help="The worker's owner API key", location='headers')
job_submit_parser.add_argument("id", type=str, required=True, help="The processing generation uuid", location="json")
job_submit_parser.add_argument("generation", type=str, required=False, default=[], help="The generated output", location="json")
class Models:
def __init__(self,api):
self.response_model_generation_result = api.model('Generation', {
'worker_id': fields.String(title="Worker ID", description="The UUID of the worker which generated this image"),
'worker_name': fields.String(title="Worker Name", description="The name of the worker which generated this image"),
})
self.response_model_wp_status_lite = api.model('RequestStatusCheck', {
'finished': fields.Integer(description="The amount of finished images in this request"),
'processing': fields.Integer(description="The amount of still processing images in this request"),
'waiting': fields.Integer(description="The amount of images waiting to be picked up by a worker"),
'done': fields.Boolean(description="True when all images in this request are done. Else False."),
'wait_time': fields.Integer(description="The expected amount to wait (in seconds) to generate all images in this request"),
'queue_position': fields.Integer(description="The position in the requests queue. This position is determined by relative Kudos amounts."),
})
self.response_model_wp_status_full = api.inherit('RequestStatus', self.response_model_wp_status_lite, {
'generations': fields.List(fields.Nested(self.response_model_generation_result)),
})
self.response_model_async = api.model('RequestAsync', {
'id': fields.String(description="The UUID of the request. Use this to retrieve the request status in the future"),
'message': fields.String(default=None,description="Any extra information from the horde about this request"),
})
self.response_model_generation_payload = api.model('ModelPayload', {
'prompt': fields.String(description="The prompt which will be sent to Stable Diffusion to generate an image"),
'n': fields.Integer(example=1, description="The amount of images to generate"),
'seed': fields.String(description="The seed to use to generete this request"),
})
self.response_model_generations_skipped = api.model('NoValidRequestFound', {
'worker_id': fields.Integer(description="How many waiting requests were skipped because they demanded a specific worker"),
})
self.response_model_job_pop = api.model('GenerationPayload', {
'payload': fields.Nested(self.response_model_generation_payload, skip_none=True),
'id': fields.String(description="The UUID for this image generation"),
'skipped': fields.Nested(self.response_model_generations_skipped, skip_none=True)
})
self.response_model_job_submit = api.model('GenerationSubmitted', {
'reward': fields.Float(example=10.0,description="The amount of kudos gained for submitting this request"),
})
self.response_model_kudos_transfer = api.model('KudosTransferred', {
'transferred': fields.Integer(example=100,description="The amount of Kudos tranferred"),
})
self.response_model_admin_maintenance = api.model('MaintenanceModeSet', {
'maintenance_mode': fields.Boolean(example=True,description="The current state of maintenance_mode"),
})
self.response_model_worker_kudos_details = api.model('WorkerKudosDetails', {
'generated': fields.Float(description="How much Kudos this worker has received for generating images"),
'uptime': fields.Integer(description="How much Kudos this worker has received for staying online longer"),
})
self.response_model_worker_details = api.model('WorkerDetails', {
"name": fields.String(description="The Name given to this worker."),
"id": fields.String(description="The UUID of this worker."),
"requests_fulfilled": fields.Integer(description="How many images this worker has generated."),
"kudos_rewards": fields.Float(description="How many Kudos this worker has been rewarded in total."),
"kudos_details": fields.Nested(self.response_model_worker_kudos_details),
"performance": fields.String(description="The average performance of this worker in human readable form."),
"uptime": fields.Integer(description="The amount of seconds this worker has been online for this Horde."),
"maintenance_mode": fields.Boolean(example=False,description="When True, this worker will not pick up any new requests"),
"paused": fields.Boolean(example=False,description="When True, this worker not be given any new requests."),
})
self.response_model_worker_modify = api.model('ModifyWorker', {
"maintenance": fields.Boolean(description="The new state of the 'maintenance' var for this worker. When True, this worker will not pick up any new requests"),
"paused": fields.Boolean(description="The new state of the 'paused' var for this worker. When True, this worker will not be given any new requests"),
})
self.response_model_user_kudos_details = api.model('UserKudosDetails', {
"accumulated": fields.Float(default=0,description="The ammount of Kudos accumulated or used for generating images."),
"gifted": fields.Float(default=0,description="The amount of Kudos this user has given to other users"),
"admin": fields.Float(default=0,description="The amount of Kudos this user has been given by the Horde admins"),
"received": fields.Float(default=0,description="The amount of Kudos this user has been given by other users"),
})
self.response_model_contrib_details = api.model('ContributionsDetails', {
"fulfillments": fields.Integer(description="How many images this user has generated")
})
self.response_model_use_details = api.model('UsageDetails', {
"requests": fields.Integer(description="How many images this user has requested")
})
self.response_model_user_details = api.model('UserDetails', {
"username": fields.String(description="The user's unique Username. It is a combination of their chosen alias plus their ID."),
"id": fields.Integer(description="The user unique ID. It is always an integer."),
"kudos": fields.Float(description="The amount of Kudos this user has. Can be negative. The amount of Kudos determines the priority when requesting image generations."),
"kudos_details": fields.Nested(self.response_model_user_kudos_details),
"usage": fields.Nested(self.response_model_use_details),
"contributions": fields.Nested(self.response_model_contrib_details),
"concurrency": fields.Integer(description="How many concurrent image generations this user may request."),
})
self.response_model_user_modify = api.model('ModifyUser', {
"new_kudos": fields.Float(description="The new total Kudos this user has after this request"),
"concurrency": fields.Integer(example=30,description="The request concurrency this user has after this request"),
"usage_multiplier": fields.Float(example=1.0,description="Multiplies the amount of kudos lost when generating images."),
})
self.response_model_horde_performance = api.model('HordePerformance', {
"queued_requests": fields.Integer(description="The amount of waiting and processing requests currently in this Horde"),
"worker_count": fields.Integer(description="How many workers are actively processing image generations in this Horde in the past 5 minutes"),
})
self.response_model_horde_maintenance_mode = api.model('HordeMaintenanceMode', {
"maintenance_mode": fields.Boolean(description="When True, this Horde will not accept new requests for image generation, but will finish processing the ones currently in the queue."),
})
self.response_model_error = api.model('RequestError', {
'message': fields.String(description="The error message for this status code."),
})
+1
View File
@@ -0,0 +1 @@
api = None
+539
View File
@@ -0,0 +1,539 @@
from flask_restx import Namespace, Resource, reqparse, fields, Api, abort
from flask import request
from ... import limiter
from ...logger import logger
from ...classes import db as _db
from ...classes import processing_generations,waiting_prompts,Worker,User,WaitingPrompt
from ... import maintenance
from enum import Enum
import os, time
api = Namespace('v1', 'API Version 1' )
response_model_generation = api.model('GenerationV1', {
'img': fields.String,
'seed': fields.String,
'server_id': fields.String(attribute='worker_id'),
'server_name': fields.String(attribute='worker_name'),
'queue_position': fields.Integer(description="The position in the requests queue. This position is determined by relative Kudos amounts."),
})
response_model_wp_status_lite = api.model('RequestStatusCheckV1', {
'finished': fields.Integer,
'processing': fields.Integer,
'waiting': fields.Integer,
'done': fields.Boolean,
'wait_time': fields.Integer,
'queue_position': fields.Integer(description="The position in the requests queue. This position is determined by relative Kudos amounts."),
})
response_model_wp_status_full = api.inherit('RequestStatusV1', response_model_wp_status_lite, {
'generations': fields.List(fields.Nested(response_model_generation)),
})
# Used to for the flas limiter, to limit requests per url paths
def get_request_path():
return(request.path)
class ServerErrors(Enum):
WRONG_CREDENTIALS = 0
INVALID_PROCGEN = 1
DUPLICATE_GEN = 2
TOO_MANY_PROMPTS = 3
EMPTY_PROMPT = 4
INVALID_API_KEY = 5
INVALID_SIZE = 6
NO_PROXY = 7
TOO_MANY_STEPS = 8
NOT_ADMIN = 9
MAINTENANCE_MODE = 10
NOT_OWNER = 11
@logger.catch
def get_error(error, **kwargs):
if error == ServerErrors.INVALID_API_KEY:
logger.warning(f'Invalid API Key sent for {kwargs["subject"]}.')
return(f'No user matching sent API Key. Have you remembered to register at https://stablehorde.net/register ?')
if error == ServerErrors.WRONG_CREDENTIALS:
logger.warning(f'User "{kwargs["username"]}" sent wrong credentials for utilizing instance {kwargs["kai_instance"]}')
return(f'wrong credentials for utilizing instance {kwargs["kai_instance"]}')
if error == ServerErrors.INVALID_PROCGEN:
logger.warning(f'Server attempted to provide generation for {kwargs["id"]} but it did not exist')
return(f'Processing Generation with ID {kwargs["id"]} does not exist')
if error == ServerErrors.DUPLICATE_GEN:
logger.warning(f'Server attempted to provide duplicate generation for {kwargs["id"]} ')
return(f'Processing Generation with ID {kwargs["id"]} already submitted')
if error == ServerErrors.TOO_MANY_PROMPTS:
logger.warning(f'User "{kwargs["username"]}" has already requested too many parallel requests ({kwargs["wp_count"]}). Aborting!')
return(f"Parallel requests exceeded user limit ({kwargs['wp_count']}). Please try again later or request to increase your concurrency.")
if error == ServerErrors.EMPTY_PROMPT:
logger.warning(f'User "{kwargs["username"]}" sent an empty prompt. Aborting!')
return("You cannot specify an empty prompt.")
if error == ServerErrors.INVALID_SIZE:
logger.warning(f'User "{kwargs["username"]}" sent an invalid size. Aborting!')
return("Invalid size. The image dimentions have to be multiples of 64.")
if error == ServerErrors.TOO_MANY_STEPS:
logger.warning(f'User "{kwargs["username"]}" sent too many steps ({kwargs["steps"]}). Aborting!')
return("Too many sampling steps. To allow resources for everyone, we allow only up to 100 steps.")
if error == ServerErrors.NO_PROXY:
logger.warning(f'Attempt to access outside reverse proxy')
return(f'Access allowed only through https')
if error == ServerErrors.NOT_ADMIN:
logger.warning(f'Non-admin user "{kwargs["username"]}" tried to use admin endpoint: "{kwargs["endpoint"]}". Aborting!')
return("You're not an admin. Sod off!")
if error == ServerErrors.MAINTENANCE_MODE:
logger.info(f'Rejecting endpoint "{kwargs["endpoint"]}" because server in maintenance mode.')
return("Server has enterred maintenance mode. Please try again later.")
if error == ServerErrors.NOT_OWNER:
logger.warning(f'User "{kwargs["username"]}" tried to modify server they do not own: "{kwargs["server_name"]}". Aborting!')
return("You're not the owner of this server!")
class SyncGenerate(Resource):
# model_generation = api.model('Successful Sync Generation', {
# 'generations': fields.List(fields.String),
# })
parser = reqparse.RequestParser()
parser.add_argument("prompt", type=str, required=True, help="The prompt to generate from")
parser.add_argument("api_key", type=str, required=True, help="The API Key corresponding to a registered user")
parser.add_argument("params", type=dict, required=False, default={}, help="Extra generate params to send to the SD server")
parser.add_argument("servers", type=str, action='append', required=False, default=[], help="If specified, only the server with this ID will be able to generate this prompt")
@api.expect(parser)
@api.response(200, 'Success', response_model_generation)
@api.response(400, 'Validation Error')
def post(self):
args = self.parser.parse_args()
username = 'Anonymous'
user = None
if maintenance.active:
return(f"{get_error(ServerErrors.MAINTENANCE_MODE, endpoint = 'SyncGenerate')}",503)
if args.api_key:
user = _db.find_user_by_api_key(args['api_key'])
if not user:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'prompt generation')}",401)
username = user.get_unique_alias()
if args['prompt'] == '':
return(f"{get_error(ServerErrors.EMPTY_PROMPT, username = username)}",400)
wp_count = waiting_prompts.count_waiting_requests(user)
if wp_count >= user.concurrency:
return(f"{get_error(ServerErrors.TOO_MANY_PROMPTS, username = username, wp_count = wp_count)}",503)
if args["params"].get("length",512)%64:
return(f"{get_error(ServerErrors.INVALID_SIZE, username = username)}",400)
if args["params"].get("width",512)%64:
return(f"{get_error(ServerErrors.INVALID_SIZE, username = username)}",400)
if args["params"].get("steps",50) > 100:
return(f"{get_error(ServerErrors.TOO_MANY_STEPS, username = username, steps = args['params']['steps'])}",400)
wp = WaitingPrompt(
_db,
waiting_prompts,
processing_generations,
args["prompt"],
user,
args["params"],
servers=args["servers"],
)
server_found = False
for server in _db.workers.values():
if len(args.servers) and server.id not in args.servers:
continue
if server.can_generate(wp)[0]:
server_found = True
break
if not server_found:
del wp # Normally garbage collection will handle it, but doesn't hurt to be thorough
return("No active server found to fulfill this request. Please Try again later...", 503)
# if a server is available to fulfil this prompt, we activate it and add it to the queue to be generated
wp.activate()
while True:
time.sleep(1)
if wp.is_stale():
return("Prompt Request Expired", 500)
if wp.is_completed():
break
ret_dict = wp.get_status()['generations']
# We delete it from memory immediately to ensure we don't run out
wp.delete()
return(ret_dict, 200)
class AsyncStatus(Resource):
decorators = [limiter.limit("2/minute", key_func = get_request_path)]
@logger.catch
@api.marshal_with(response_model_wp_status_full, code=200, description='Images Generated')
def get(self, id = ''):
wp = waiting_prompts.get_item(id)
if not wp:
return("ID not found", 404)
wp_status = wp.get_status()
# If the status is retrieved after the wp is done we clear it to free the ram
if wp_status["done"]:
wp.delete()
return(wp_status, 200)
class AsyncCheck(Resource):
# Increasing this until I can figure out how to pass original IP from reverse proxy
decorators = [limiter.limit("10/second")]
@logger.catch
def get(self, id = ''):
wp = waiting_prompts.get_item(id)
if not wp:
return("ID not found", 404)
return(wp.get_lite_status(), 200)
class AsyncGenerate(Resource):
parser = reqparse.RequestParser()
parser.add_argument("prompt", type=str, required=True, help="The prompt to generate from")
parser.add_argument("api_key", type=str, required=True, help="The API Key corresponding to a registered user")
parser.add_argument("params", type=dict, required=False, default={}, help="Extra generate params to send to the SD server")
parser.add_argument("servers", type=str, action='append', required=False, default=[], help="If specified, only the server with this ID will be able to generate this prompt")
@api.expect(parser)
def post(self):
args = self.parser.parse_args()
username = 'Anonymous'
user = None
if maintenance.active:
return(f"{get_error(ServerErrors.MAINTENANCE_MODE, endpoint = 'AsyncGenerate')}",503)
if args.api_key:
user = _db.find_user_by_api_key(args['api_key'])
if not user:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'prompt generation')}",401)
username = user.get_unique_alias()
if args['prompt'] == '':
return(f"{get_error(ServerErrors.EMPTY_PROMPT, username = username)}",400)
wp_count = waiting_prompts.count_waiting_requests(user)
if wp_count >= user.concurrency:
return(f"{get_error(ServerErrors.TOO_MANY_PROMPTS, username = username, wp_count = wp_count)}",503)
if args["params"].get("length",512)%64:
return(f"{get_error(ServerErrors.INVALID_SIZE, username = username)}",400)
if args["params"].get("width",512)%64:
return(f"{get_error(ServerErrors.INVALID_SIZE, username = username)}",400)
if args["params"].get("steps",50) > 100:
return(f"{get_error(ServerErrors.TOO_MANY_STEPS, username = username, steps = args['params']['steps'])}",400)
wp = WaitingPrompt(
_db,
waiting_prompts,
processing_generations,
args["prompt"],
user,
args["params"],
servers=args["servers"],
)
server_found = False
for server in _db.workers.values():
if len(args.servers) and server.id not in args.servers:
continue
if server.can_generate(wp)[0]:
server_found = True
break
if not server_found:
del wp # Normally garbage collection will handle it, but doesn't hurt to be thorough
return("No active server found to fulfill this request. Please Try again later...", 503)
# if a server is available to fulfil this prompt, we activate it and add it to the queue to be generated
wp.activate()
return({"id":wp.id}, 200)
class PromptPop(Resource):
parser = reqparse.RequestParser()
parser.add_argument("api_key", type=str, required=True, help="The API Key corresponding to a registered user")
parser.add_argument("name", type=str, required=True, help="The server's unique name, to track contributions")
parser.add_argument("max_pixels", type=int, required=False, default=512, help="The maximum amount of pixels this server can generate")
parser.add_argument("priority_usernames", type=str, action='append', required=False, default=[], help="The usernames which get priority use on this server")
decorators = [limiter.limit("45/second")]
@api.expect(parser)
def post(self):
args = self.parser.parse_args()
skipped = {}
user = _db.find_user_by_api_key(args['api_key'])
if not user:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'server promptpop: ' + args['name'])}",401)
server = _db.find_worker_by_name(args['name'])
if not server:
server = Worker(_db)
server.create(user, args['name'])
if user != server.user:
return(f"{get_error(ServerErrors.WRONG_CREDENTIALS,kai_instance = args['name'], username = user.get_unique_alias())}",401)
server.check_in(args['max_pixels'])
if server.maintenance:
return(f"Server has been put into maintenance mode by the owner",403)
if server.paused:
return({"id": None, "skipped": {}},200)
# This ensures that the priority requested by the bridge is respected
prioritized_wp = []
priority_users = [user]
## Start prioritize by bridge request ##
for priority_username in args.priority_usernames:
priority_user = _db.find_user_by_username(priority_username)
if priority_user:
priority_users.append(priority_user)
for priority_user in priority_users:
for wp in waiting_prompts.get_all():
if wp.user == priority_user and wp.needs_gen():
prioritized_wp.append(wp)
## End prioritize by bridge request ##
for wp in waiting_prompts.get_waiting_wp_by_kudos():
if wp not in prioritized_wp:
prioritized_wp.append(wp)
for wp in prioritized_wp:
check_gen = server.can_generate(wp)
if not check_gen[0]:
skipped_reason = check_gen[1]
skipped[skipped_reason] = skipped.get(skipped_reason,0) + 1
continue
ret = wp.start_generation(server)
return(ret, 200)
return({"id": None, "skipped": skipped}, 200)
class SubmitGeneration(Resource):
parser = reqparse.RequestParser()
parser.add_argument("id", type=str, required=True, help="The processing generation uuid")
parser.add_argument("api_key", type=str, required=True, help="The server's owner API key")
parser.add_argument("generation", type=str, required=False, default=[], help="The download location of the image")
parser.add_argument("seed", type=str, required=True, default=[], help="The seed of the generated image")
@api.expect(parser)
def post(self):
args = self.parser.parse_args()
procgen = processing_generations.get_item(args['id'])
if not procgen:
return(f"{get_error(ServerErrors.INVALID_PROCGEN,id = args['id'])}",404)
user = _db.find_user_by_api_key(args['api_key'])
if not user:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'server submit: ' + args['name'])}",401)
if user != procgen.worker.user:
return(f"{get_error(ServerErrors.WRONG_CREDENTIALS,kai_instance = args['name'], username = user.get_unique_alias())}",401)
kudos = procgen.set_generation(args['generation'], args['seed'])
if kudos == 0:
return(f"{get_error(ServerErrors.DUPLICATE_GEN,id = args['id'])}",400)
return({"reward": kudos}, 200)
class TransferKudos(Resource):
parser = reqparse.RequestParser()
parser.add_argument("username", type=str, required=True, help="The user ID which will receive the kudos")
parser.add_argument("api_key", type=str, required=True, help="The sending user's API key")
parser.add_argument("amount", type=int, required=False, default=100, help="The amount of kudos to transfer")
@api.expect(parser)
def post(self):
args = self.parser.parse_args()
user = _db.find_user_by_api_key(args['api_key'])
if not user:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'kudos transfer to: ' + args['username'])}",401)
ret = _db.transfer_kudos_from_apikey_to_username(args['api_key'],args['username'],args['amount'])
kudos = ret[0]
error = ret[1]
if error != 'OK':
return(f"{error}",400)
return({"transfered": kudos}, 200)
class AdminMaintenanceMode(Resource):
parser = reqparse.RequestParser()
parser.add_argument("api_key", type=str, required=True, help="The Admin API key")
parser.add_argument("active", type=bool, required=True, help="Star or stop maintenance mode")
decorators = [limiter.limit("30/minute")]
@api.expect(parser)
def put(self):
args = self.parser.parse_args()
admin = _db.find_user_by_api_key(args['api_key'])
if not admin:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'Admin action: ' + 'AdminMaintenanceMode')}",401)
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
return(f"{get_error(ServerErrors.NOT_ADMIN, username = admin.get_unique_alias(), endpoint = 'AdminMaintenanceMode')}",401)
logger.debug(maintenance)
maintenance.toggle(args['active'])
return({"maintenance_mode": maintenance.active}, 200)
class Servers(Resource):
@logger.catch
def get(self):
servers_ret = []
for server in _db.workers.values():
if server.is_stale():
continue
sdict = {
"name": server.name,
"id": server.id,
"max_pixels": server.max_pixels,
"megapixelsteps_generated": server.contributions,
"requests_fulfilled": server.fulfilments,
"kudos_rewards": server.kudos,
"kudos_details": server.kudos_details,
"performance": server.get_performance(),
"uptime": server.uptime,
"maintenance_mode": server.maintenance,
}
servers_ret.append(sdict)
return(servers_ret,200)
class ServerSingle(Resource):
@logger.catch
def get(self, server_id = ''):
server = _db.find_worker_by_id(server_id)
if server:
sdict = {
"name": server.name,
"id": server.id,
"max_pixels": server.max_pixels,
"megapixelsteps_generated": server.contributions,
"requests_fulfilled": server.fulfilments,
"latest_performance": server.get_performance(),
"maintenance_mode": server.maintenance,
}
return(sdict,200)
else:
return("Not found", 404)
parser = reqparse.RequestParser()
parser.add_argument("api_key", type=str, required=True, help="The Admin or server owner API key")
parser.add_argument("maintenance", type=bool, required=False, help="Set to true to put this server into maintenance.")
parser.add_argument("paused", type=bool, required=False, help="Set to true to pause this server.")
decorators = [limiter.limit("30/minute")]
@api.expect(parser)
def put(self, server_id = ''):
server = _db.find_worker_by_id(server_id)
if not server:
return("Invalid Server ID", 404)
args = self.parser.parse_args()
admin = _db.find_user_by_api_key(args['api_key'])
if not admin:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'User action: ' + 'PUT ServerSingle')}",401)
ret_dict = {}
# Both admins and owners can set the server to maintenance
if args.maintenance != None:
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
if admin != server.user:
return(f"{get_error(ServerErrors.NOT_OWNER, username = admin.get_unique_alias(), server_name = server.name)}",401)
server.maintenance = args.maintenance
ret_dict["maintenance"] = server.maintenance
# Only admins can set a server as paused
if args.paused != None:
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
return(f"{get_error(ServerErrors.NOT_ADMIN, username = admin.get_unique_alias(), endpoint = 'AdminModifyServer')}",401)
server.paused = args.paused
ret_dict["paused"] = server.paused
if not len(ret_dict):
return("No server modification selected!", 400)
return(ret_dict, 200)
# parser = reqparse.RequestParser()
# parser.add_argument("api_key", type=str, required=True, help="The Admin or server owner API key")
# # post shows also hidden server info
# decorators = [limiter.limit("30/minute")]
# def post(self, server_id = ''):
# server = _db.find_worker_by_id(server_id)
# if not server:
# return("Invalid Server ID", 404)
# args = self.parser.parse_args()
# admin = _db.find_user_by_api_key(args['api_key'])
# if not admin:
# return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'User action: ' + 'PUT ServerSingle')}",401)
# sdict = {
# "name": server.name,
# "id": server.id,
# "max_pixels": server.max_pixels,
# "megapixelsteps_generated": server.contributions,
# "requests_fulfilled": server.fulfilments,
# "latest_performance": server.get_performance(),
# "maintenance": server.maintenance,
# "paused": server.paused,
# "owner": server.user.get_unique_alias(),
# }
# return(sdict,200)
class Users(Resource):
@logger.catch
def get(self):
user_dict = {}
for user in _db.users.values():
user_dict[user.get_unique_alias()] = {
"id": user.id,
"kudos": user.kudos,
"kudos_details": user.kudos_details,
"usage": user.usage,
"contributions": user.contributions,
"concurrency": user.concurrency,
}
return(user_dict,200)
class UserSingle(Resource):
@logger.catch
def get(self, user_id = ''):
logger.debug(user_id)
user = _db.find_user_by_id(user_id)
if user:
udict = {
"username": user.get_unique_alias(),
"kudos": user.kudos,
"usage": user.usage,
"contributions": user.contributions,
"concurrency": user.concurrency,
}
return(udict,200)
else:
return("Not found", 404)
parser = reqparse.RequestParser()
parser.add_argument("api_key", type=str, required=True, help="The Admin API key")
parser.add_argument("kudos", type=int, required=False, help="The amount of kudos to modify (can be negative)")
parser.add_argument("concurrency", type=int, required=False, help="The amount of concurrent request this user can have")
parser.add_argument("usage_multiplier", type=float, required=False, help="The amount by which to multiply the users kudos consumption")
decorators = [limiter.limit("30/minute")]
@api.expect(parser)
def put(self, user_id = ''):
user = user = _db.find_user_by_id(user_id)
if not user:
return(f"Invalid user_id: {user_id}",400)
args = self.parser.parse_args()
admin = _db.find_user_by_api_key(args['api_key'])
if not admin:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'Admin action: ' + 'PUT UserSingle')}",401)
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
return(f"{get_error(ServerErrors.NOT_ADMIN, username = admin.get_unique_alias(), endpoint = 'AdminModifyUser')}",401)
ret_dict = {}
if args.kudos:
user.modify_kudos(args.kudos, 'admin')
ret_dict["new_kudos"] = user.kudos
if args.concurrency:
user.concurrency = args.concurrency
ret_dict["concurrency"] = user.concurrency
if args.usage_multiplier:
user.usage_multiplier = args.usage_multiplier
ret_dict["usage_multiplier"] = user.usage_multiplier
if not len(ret_dict):
return("No usermod operations selected!", 400)
return(ret_dict, 200)
class HordeLoad(Resource):
@logger.catch
def get(self):
load_dict = waiting_prompts.count_totals()
load_dict["megapixelsteps_per_min"] = _db.stats.get_things_per_min()
load_dict["server_count"] = _db.count_active_workers()
load_dict["maintenance_mode"] = maintenance.active
return(load_dict,200)
api.add_resource(SyncGenerate, "/generate/sync")
# Async is disabled due to the memory requirements of keeping images in running memory
api.add_resource(AsyncGenerate, "/generate/async")
api.add_resource(AsyncStatus, "/generate/prompt/<string:id>")
api.add_resource(AsyncCheck, "/generate/check/<string:id>")
api.add_resource(PromptPop, "/generate/pop")
api.add_resource(SubmitGeneration, "/generate/submit")
api.add_resource(Users, "/users")
api.add_resource(UserSingle, "/users/<string:user_id>")
api.add_resource(Servers, "/servers")
api.add_resource(ServerSingle, "/servers/<string:server_id>")
api.add_resource(TransferKudos, "/kudos/transfer")
api.add_resource(HordeLoad, "/status/performance")
api.add_resource(AdminMaintenanceMode, "/admin/maintenance")
+1
View File
@@ -0,0 +1 @@
api = None
+56
View File
@@ -0,0 +1,56 @@
from .v2 import *
class AsyncGenerate(AsyncGenerate):
def validate(self):
super().validate()
if self.args["params"].get("length",512)%64:
raise e.InvalidSize(self.username)
if self.args["params"].get("width",512)%64:
raise e.InvalidSize(self.username)
if self.args["params"].get("steps",50) > 100:
raise e.TooManySteps(self.username, self.args['params']['steps'])
class SyncGenerate(SyncGenerate):
def validate(self):
super().validate()
if self.args["params"].get("length",512)%64:
raise e.InvalidSize(self.username)
if self.args["params"].get("width",512)%64:
raise e.InvalidSize(self.username)
if self.args["params"].get("steps",50) > 100:
raise e.TooManySteps(self.username, self.args['params']['steps'])
class JobPop(JobPop):
def check_in(self):
self.worker.check_in(self.args['max_pixels'])
class HordeLoad(HordeLoad):
decorators = [limiter.limit("2/second")]
# When we extend the actual method, we need to re-apply the decorators
@logger.catch
@api.marshal_with(models.response_model_horde_maintenance_mode, code=200, description='Horde Maintenance')
def get(self):
'''Details about the current performance of this Horde
'''
load_dict = super().get()
load_dict["past_minute_megapixelsteps"] = db.stats.get_things_per_min()
return(load_dict,200)
api.add_resource(SyncGenerate, "/generate/sync")
api.add_resource(AsyncGenerate, "/generate/async")
api.add_resource(AsyncStatus, "/generate/status/<string:id>")
api.add_resource(AsyncCheck, "/generate/check/<string:id>")
api.add_resource(JobPop, "/generate/pop")
api.add_resource(JobSubmit, "/generate/submit")
api.add_resource(Users, "/users")
api.add_resource(UserSingle, "/users/<string:user_id>")
api.add_resource(Workers, "/workers")
api.add_resource(WorkerSingle, "/workers/<string:worker_id>")
api.add_resource(TransferKudos, "/kudos/transfer")
api.add_resource(HordeLoad, "/status/performance")
api.add_resource(HordeMaintenance, "/status/maintenance")
+518
View File
@@ -0,0 +1,518 @@
from flask_restx import Namespace, Resource, reqparse, fields, Api, abort
from flask import request
from ... import limiter
from ...logger import logger
from ...classes import db
from ...classes import processing_generations,waiting_prompts,Worker,User,WaitingPrompt
from ... import maintenance
from enum import Enum
from .. import exceptions as e
import os, time
from .. import ModelsV2, ParsersV2
api = Namespace('v2', 'API Version 2' )
models = ModelsV2(api)
parsers = ParsersV2()
handle_missing_prompts = api.errorhandler(e.MissingPrompt)(e.handle_bad_requests)
handle_kudos_validation_error = api.errorhandler(e.KudosValidationError)(e.handle_bad_requests)
handle_invalid_size = api.errorhandler(e.InvalidSize)(e.handle_bad_requests)
handle_too_many_steps = api.errorhandler(e.TooManySteps)(e.handle_bad_requests)
handle_invalid_api = api.errorhandler(e.InvalidAPIKey)(e.handle_bad_requests)
handle_wrong_credentials = api.errorhandler(e.WrongCredentials)(e.handle_bad_requests)
handle_not_admin = api.errorhandler(e.NotAdmin)(e.handle_bad_requests)
handle_not_owner = api.errorhandler(e.NotOwner)(e.handle_bad_requests)
handle_worker_maintenance = api.errorhandler(e.WorkerMaintenance)(e.handle_bad_requests)
handle_invalid_procgen = api.errorhandler(e.InvalidProcGen)(e.handle_bad_requests)
handle_request_not_found = api.errorhandler(e.RequestNotFound)(e.handle_bad_requests)
handle_worker_not_found = api.errorhandler(e.WorkerNotFound)(e.handle_bad_requests)
handle_user_not_found = api.errorhandler(e.UserNotFound)(e.handle_bad_requests)
handle_duplicate_gen = api.errorhandler(e.DuplicateGen)(e.handle_bad_requests)
handle_too_many_prompts = api.errorhandler(e.TooManyPrompts)(e.handle_bad_requests)
handle_no_valid_workers = api.errorhandler(e.NoValidWorkers)(e.handle_bad_requests)
handle_maintenance_mode = api.errorhandler(e.MaintenanceMode)(e.handle_bad_requests)
# Used to for the flas limiter, to limit requests per url paths
def get_request_path():
return(request.path)
# I have to put it outside the class as I can't figure out how to extend the argparser and also pass it to the @api.expect decorator inside the class
class GenerateTemplate(Resource):
def post(self):
self.args = parsers.generate_parser.parse_args()
self.username = 'Anonymous'
self.user = None
self.validate()
self.initiate_waiting_prompt()
worker_found = False
for worker in db.workers.values():
if len(self.args.workers) and worker.id not in self.args.workers:
continue
if worker.can_generate(self.wp)[0]:
worker_found = True
break
self.activate_waiting_prompt()
# We split this into its own function, so that it may be overriden and extended
def validate(self):
if maintenance.active:
raise e.MaintenanceMode('SyncGenerate')
if self.args.apikey:
self.user = db.find_user_by_api_key(self.args['apikey'])
if not self.user:
raise e.InvalidAPIKey('async generation')
self.username = self.user.get_unique_alias()
if self.args['prompt'] == '':
raise e.MissingPrompt(self.username)
wp_count = waiting_prompts.count_waiting_requests(self.user)
if wp_count >= self.user.concurrency:
raise e.TooManyPrompts(self.username, wp_count)
# We split this into its own function, so that it may be overriden
def initiate_waiting_prompt(self):
self.wp = WaitingPrompt(
db,
waiting_prompts,
processing_generations,
self.args["prompt"],
self.user,
self.args["params"],
workers=self.args["workers"],
)
# We split this into its own function, so that it may be overriden and extended
def activate_waiting_prompt(self):
self.wp.activate()
def has_valid_workers(self):
worker_found = False
for worker in db.workers.values():
if len(self.args.workers) and worker.id not in self.args.workers:
continue
if worker.can_generate(self.wp)[0]:
worker_found = True
break
return(worker_found)
class AsyncGenerate(GenerateTemplate):
@api.expect(parsers.generate_parser)
@api.marshal_with(models.response_model_async, code=202, description='Generation Queued', skip_none=True)
@api.response(400, 'Validation Error', models.response_model_error)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(503, 'Maintenance Mode', models.response_model_error)
@api.response(429, 'Too Many Prompts', models.response_model_error)
def post(self):
'''Initiate an Asynchronous request to generate images.
This endpoint will immediately return with the UUID of the request for generation.
This endpoint will always be accepted, even if there are no workers available currently to fulfill this request.
Perhaps some will appear in the next 10 minutes.
Asynchronous requests live for 10 minutes before being considered stale and being deleted.
'''
super().post()
ret_dict = {"id":self.wp.id}
if not self.has_valid_workers():
ret_dict['message'] = "Warning: No available workers can fulfill this request. It will expire in 10 minutes. Consider reducing the size to 512x512"
return(ret_dict, 202)
class SyncGenerate(GenerateTemplate):
@api.expect(parsers.generate_parser)
# If I marshal it here, it overrides the marshalling of the child class unfortunately
@api.marshal_with(models.response_model_wp_status_full, code=200, description='Images Generated')
@api.response(400, 'Validation Error', models.response_model_error)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(503, 'Maintenance Mode', models.response_model_error)
@api.response(429, 'Too Many Prompts', models.response_model_error)
def post(self):
'''Initiate a Synchronous request to generate images.
This connection will only terminate when the images have been generated, or an error occured.
If you connection is interrupted, you will not have the request UUID, so you cannot retrieve the images asynchronously.
'''
super().post()
while True:
time.sleep(1)
if self.wp.is_stale():
raise e.RequestExpired(self.username)
if self.wp.is_completed():
break
ret_dict = self.wp.get_status()
# We delete it from memory immediately to ensure we don't run out
self.wp.delete()
return(ret_dict, 200)
# We extend this function so we can check if any workers can fulfil the request, before adding it to the queue
def activate_waiting_prompt(self):
# We don't want to keep synchronous requests up unless there's someone who can fulfill them
if not self.has_valid_workers():
# We don't need to call .delete() on the wp because it's not activated yet
# And therefore not added to the waiting_prompt dict.
raise e.NoValidWorkers(self.username)
# if a worker is available to fulfil this prompt, we activate it and add it to the queue to be generated
super().activate_waiting_prompt()
class AsyncStatus(Resource):
decorators = [limiter.limit("2/minute", key_func = get_request_path)]
# If I marshal it here, it overrides the marshalling of the child class unfortunately
@api.marshal_with(models.response_model_wp_status_full, code=200, description='Async Request Full Status')
@api.response(404, 'Request Not found', models.response_model_error)
def get(self, id = ''):
'''Retrieve the full status of an Asynchronous generation request.
This request will include all already generated images in base64 encoded .webp files.
As such, you are requested to not retrieve this endpoint often. Instead use the /check/ endpoint first
This endpoint is limited to 1 request per minute
'''
wp = waiting_prompts.get_item(id)
if not wp:
raise e.RequestNotFound(id)
wp_status = wp.get_status()
# If the status is retrieved after the wp is done we clear it to free the ram
if wp_status["done"]:
wp.delete()
return(wp_status, 200)
@api.marshal_with(models.response_model_wp_status_full, code=200, description='Async Request Full Status')
@api.response(404, 'Request Not found', models.response_model_error)
def delete(self, id = ''):
'''Cancel an unfinished request.
This request will include all already generated images in base64 encoded .webp files.
'''
wp = waiting_prompts.get_item(id)
if not wp:
raise e.RequestNotFound(id)
wp_status = wp.get_status()
logger.info(f"Request with ID {wp.id} has been cancelled.")
wp.delete()
return(wp_status, 200)
class AsyncCheck(Resource):
# Increasing this until I can figure out how to pass original IP from reverse proxy
decorators = [limiter.limit("10/second")]
@api.marshal_with(models.response_model_wp_status_lite, code=200, description='Async Request Status Check')
@api.response(404, 'Request Not found', models.response_model_error)
def get(self, id = ''):
'''Retrieve the status of an Asynchronous generation request without images.
Use this request to check the status of a currently running asynchronous request without consuming bandwidth.
'''
wp = waiting_prompts.get_item(id)
if not wp:
raise e.RequestNotFound(id)
return(wp.get_lite_status(), 200)
class JobPop(Resource):
decorators = [limiter.limit("2/second")]
@api.expect(parsers.job_pop_parser)
@api.marshal_with(models.response_model_job_pop, code=200, description='Generation Popped')
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(403, 'Access Denied', models.response_model_error)
def post(self):
'''Check if there are generation requests queued for fulfillment.
This endpoint is used by registered workers only
'''
self.args = parsers.job_pop_parser.parse_args()
self.validate()
self.check_in()
# Paused worker return silently
if self.worker.paused:
return({"id": None, "skipped": {}},200)
# This ensures that the priority requested by the bridge is respected
self.prioritized_wp = []
self.priority_users = [self.user]
## Start prioritize by bridge request ##
for priority_username in self.args.priority_usernames:
priority_user = db.find_user_by_username(priority_username)
if priority_user:
self.priority_users.append(priority_user)
for priority_user in self.priority_users:
for wp in waiting_prompts.get_all():
if wp.user == priority_user and wp.needs_gen():
self.prioritized_wp.append(wp)
## End prioritize by bridge request ##
for wp in waiting_prompts.get_waiting_wp_by_kudos():
if wp not in self.prioritized_wp:
self.prioritized_wp.append(wp)
for wp in self.prioritized_wp:
check_gen = self.worker.can_generate(wp)
if not check_gen[0]:
skipped_reason = check_gen[1]
self.skipped[skipped_reason] = self.skipped.get(skipped_reason,0) + 1
continue
ret = wp.start_generation(self.worker)
return(ret, 200)
return({"id": None, "skipped": self.skipped}, 200)
# We split this into its own function, so that it may be overriden and extended
def validate(self):
self.skipped = {}
self.user = db.find_user_by_api_key(self.args['apikey'])
if not self.user:
raise e.InvalidAPIKey('prompt pop')
self.worker = db.find_worker_by_name(self.args['name'])
if not self.worker:
self.worker = Worker(db)
self.worker.create(self.user, self.args['name'])
if self.user != self.worker.user:
raise e.WrongCredentials(self.user.get_unique_alias(), self.args['name'])
if self.worker.maintenance:
raise e.WorkerMaintenance()
# We split this to its own function so that it can be extended with the specific vars needed to check in
# You typically never want to use this template's function without extending it
def check_in(self):
self.worker.check_in()
class JobSubmit(Resource):
@api.expect(parsers.job_submit_parser)
@api.marshal_with(models.response_model_job_submit, code=200, description='Generation Submitted')
@api.response(400, 'Generation Already Submitted', models.response_model_error)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(402, 'Access Denied', models.response_model_error)
@api.response(404, 'Request Not Found', models.response_model_error)
def post(self):
'''Submit a generated image.
This endpoint is used by registered workers only
'''
self.args = parsers.job_submit_parser.parse_args()
self.validate()
return({"reward": self.kudos}, 200)
def validate(self):
self.procgen = processing_generations.get_item(self.args['id'])
if not self.procgen:
raise e.InvalidProcGen(self.args['id'])
self.user = db.find_user_by_api_key(self.args['apikey'])
if not self.user:
raise e.InvalidAPIKey('worker submit:' + self.args['name'])
if self.user != self.procgen.worker.user:
raise e.WrongCredentials(user.get_unique_alias(), self.args['name'])
self.kudos = self.procgen.set_generation(self.args['generation'], self.args['seed'])
if self.kudos == 0:
raise e.DuplicateGen(self.procgen.worker.name, self.args['id'])
class TransferKudos(Resource):
parser = reqparse.RequestParser()
parser.add_argument("apikey", type=str, required=True, help="The sending user's API key", location='headers')
parser.add_argument("username", type=str, required=True, help="The user ID which will receive the kudos", location="json")
parser.add_argument("amount", type=int, required=False, default=100, help="The amount of kudos to transfer", location="json")
@api.expect(parser)
@api.marshal_with(models.response_model_kudos_transfer, code=200, description='Generation Submitted')
@api.response(400, 'Validation Error', models.response_model_error)
@api.response(401, 'Invalid API Key', models.response_model_error)
def post(self):
'''Transfer Kudos to another registed user
'''
self.args = self.parser.parse_args()
user = db.find_user_by_api_key(self.args['apikey'])
if not user:
raise e.InvalidAPIKey('kudos transfer to: ' + self.args['username'])
ret = db.transfer_kudos_from_apikey_to_username(self.args['apikey'],self.args['username'],self.args['amount'])
kudos = ret[0]
error = ret[1]
if error != 'OK':
raise e.KudosValidationError(user.get_unique_alias(), error)
return({"transfered": kudos}, 200)
class Workers(Resource):
@logger.catch
@api.marshal_with(models.response_model_worker_details, code=200, description='Workers List', as_list=True, skip_none=True)
def get(self):
'''A List with the details of all registered and active workers
'''
workers_ret = []
# I could do this with a comprehension, but this is clearer to understand
for worker in db.workers.values():
if worker.is_stale():
continue
workers_ret.append(worker.get_details())
return(workers_ret,200)
class WorkerSingle(Resource):
get_parser = reqparse.RequestParser()
get_parser.add_argument("apikey", type=str, required=False, help="The Admin or Owner API key", location='headers')
@api.marshal_with(models.response_model_worker_details, code=200, description='Worker Details', skip_none=True)
@api.response(404, 'Worker Not Found', models.response_model_error)
def get(self, worker_id = ''):
'''Details of a registered worker
Can retrieve the details of a worker even if inactive
(A worker is considered inactive if it has not checked in for 5 minutes)
'''
worker = db.find_worker_by_id(worker_id)
if not worker:
raise e.WorkerNotFound(worker_id)
is_privileged = False
self.args = self.get_parser.parse_args()
if self.args.apikey:
admin = db.find_user_by_api_key(self.args['apikey'])
if not admin:
raise e.InvalidAPIKey('admin worker details')
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
raise e.NotAdmin(admin.get_unique_alias(), 'AdminWorkerDetails')
is_privileged = True
return(worker.get_details(is_privileged),200)
put_parser = reqparse.RequestParser()
put_parser.add_argument("apikey", type=str, required=True, help="The Admin or Owner API key", location='headers')
put_parser.add_argument("maintenance", type=bool, required=False, help="Set to true to put this worker into maintenance.", location="json")
put_parser.add_argument("paused", type=bool, required=False, help="Set to true to pause this worker.", location="json")
decorators = [limiter.limit("30/minute")]
# @api.expect(parser)
@api.marshal_with(models.response_model_worker_modify, code=200, description='Modify Worker', skip_none=True)
@api.response(400, 'Validation Error', models.response_model_error)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(402, 'Access Denied', models.response_model_error)
@api.response(404, 'Worker Not Found', models.response_model_error)
def put(self, worker_id = ''):
'''Put the worker into maintenance or pause mode
Maintenance can be set by the owner of the serve or an admin.
When in maintenance, the worker will receive a 503 request when trying to retrieve new requests. Use this to avoid disconnecting your worker in the middle of a generation
Paused can be set only by the admins of this Horde.
When in paused mode, the worker will not be given any requests to generate.
'''
worker = db.find_worker_by_id(worker_id)
if not worker:
raise e.WorkerNotFound(worker_id)
self.args = self.put_parser.parse_args()
admin = db.find_user_by_api_key(self.args['apikey'])
if not admin:
raise e.InvalidAPIKey('User action: ' + 'PUT WorkerSingle')
ret_dict = {}
# Both admins and owners can set the worker to maintenance
if self.args.maintenance != None:
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
if admin != worker.user:
raise e.NotOwner(admin.get_unique_alias(), worker.name)
worker.maintenance = self.args.maintenance
ret_dict["maintenance"] = worker.maintenance
# Only admins can set a worker as paused
if self.args.paused != None:
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
raise e.NotAdmin(admin.get_unique_alias(), 'AdminModifyWorker')
worker.paused = self.args.paused
ret_dict["paused"] = worker.paused
if not len(ret_dict):
raise e.NoValidActions("No worker modification selected!")
return(ret_dict, 200)
class Users(Resource):
decorators = [limiter.limit("2/minute")]
@logger.catch
@api.marshal_with(models.response_model_user_details, code=200, description='Users List')
def get(self):
'''A List with the details and statistic of all registered users
'''
users_list = [user.get_details() for user in db.users.values()]
return(users_list,200)
class UserSingle(Resource):
decorators = [limiter.limit("30/minute")]
@api.marshal_with(models.response_model_user_details, code=200, description='User Details')
@api.response(404, 'User Not Found', models.response_model_error)
def get(self, user_id = ''):
'''Details and statistics about a specific user
'''
user = db.find_user_by_id(user_id)
if not user:
raise e.UserNotFound(user_id)
return(user.get_details(),200)
parser = reqparse.RequestParser()
parser.add_argument("apikey", type=str, required=True, help="The Admin API key", location='headers')
parser.add_argument("kudos", type=int, required=False, help="The amount of kudos to modify (can be negative)", location="json")
parser.add_argument("concurrency", type=int, required=False, help="The amount of concurrent request this user can have", location="json")
parser.add_argument("usage_multiplier", type=float, required=False, help="The amount by which to multiply the users kudos consumption", location="json")
decorators = [limiter.limit("30/minute")]
@api.expect(parser)
@api.marshal_with(models.response_model_user_modify, code=200, description='Modify User', skip_none=True)
@api.response(400, 'Validation Error', models.response_model_error)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(402, 'Access Denied', models.response_model_error)
@api.response(404, 'Worker Not Found', models.response_model_error)
def put(self, user_id = ''):
'''Endpoint for horde admins to perform operations on users
'''
user = user = db.find_user_by_id(user_id)
if not user:
raise e.UserNotFound(user_id)
self.args = self.parser.parse_args()
admin = db.find_user_by_api_key(self.args['apikey'])
if not admin:
raise e.InvalidAPIKey('Admin action: ' + 'PUT UserSingle')
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
raise e.NotAdmin(admin.get_unique_alias(), 'AdminModifyUser')
ret_dict = {}
if self.args.kudos:
user.modify_kudos(self.args.kudos, 'admin')
ret_dict["new_kudos"] = user.kudos
if self.args.concurrency:
user.concurrency = self.args.concurrency
ret_dict["concurrency"] = user.concurrency
if self.args.usage_multiplier:
user.usage_multiplier = self.args.usage_multiplier
ret_dict["usage_multiplier"] = user.usage_multiplier
if not len(ret_dict):
raise e.NoValidActions("No usermod operations selected!")
return(ret_dict, 200)
class HordeLoad(Resource):
decorators = [limiter.limit("20/minute")]
@logger.catch
@api.marshal_with(models.response_model_horde_performance, code=200, description='Horde Performance')
def get(self):
'''Details about the current performance of this Horde
'''
load_dict = waiting_prompts.count_totals()
load_dict["worker_count"] = db.count_active_workers()
return(load_dict,200)
class HordeMaintenance(Resource):
decorators = [limiter.limit("2/second")]
@logger.catch
@api.marshal_with(models.response_model_horde_maintenance_mode, code=200, description='Horde Maintenance')
def get(self):
'''Horde Maintenance Mode Status
Use this endpoint to quicky determine if this horde is in maintenance.
'''
ret_dict = {
"maintenance_mode": maintenance.active
}
return(ret_dict,200)
parser = reqparse.RequestParser()
parser.add_argument("apikey", type=str, required=True, help="The Admin API key", location="headers")
parser.add_argument("active", type=bool, required=True, help="Star or stop maintenance mode", location="json")
decorators = [limiter.limit("30/minute")]
@api.expect(parser)
@api.marshal_with(models.response_model_admin_maintenance, code=200, description='Maintenance Mode Set')
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(402, 'Access Denied', models.response_model_error)
def put(self):
'''Change Horde Maintenance Mode
Endpoint for admins to (un)set the horde into maintenance.
When in maintenance no new requests for generation will be accepted
but requests currently in the queue will be completed.
'''
self.args = self.parser.parse_args()
admin = db.find_user_by_api_key(self.args['apikey'])
if not admin:
raise e.InvalidAPIKey('Admin action: ' + 'AdminMaintenanceMode')
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
raise e.NotAdmin(admin.get_unique_alias(), 'AdminMaintenanceMode')
maintenance.toggle(self.args['active'])
return({"maintenance_mode": maintenance.active}, 200)
+10
View File
@@ -0,0 +1,10 @@
import argparse
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('-i', '--insecure', action="store_true", help="If set, will use http instead of https (useful for testing)")
arg_parser.add_argument('-v', '--verbosity', action='count', default=0, help="The default logging level is ERROR or higher. This value increases the amount of logging seen in your screen")
arg_parser.add_argument('-q', '--quiet', action='count', default=0, help="The default logging level is ERROR or higher. This value decreases the amount of logging seen in your screen")
arg_parser.add_argument('-c', '--convert_flag', action='store', default=None, required=False, type=str, help="A special flag to convert from previous DB entries to newer and exit")
arg_parser.add_argument('-p', '--port', action='store', default=7001, required=False, type=int, help="Provide a different port to start with")
arg_parser.add_argument('--horde', action='store', default='stable', required=True, type=str, help="Which Horde to Start. This customizes endpoints and methods")
args = arg_parser.parse_args()
+29
View File
@@ -0,0 +1,29 @@
from .. import logger, args
from importlib import import_module
thing_names = {
"stable": "megapixelsteps",
"kobold": "kilotokens",
}
raw_thing_names = {
"stable": "pixelsteps",
"kobold": "tokens",
}
thing_name = thing_names[args.horde]
raw_thing_name = raw_thing_names[args.horde]
# Should figure out an elegant way to do this with a for loop
WaitingPrompt = import_module(name=f'horde.classes.{args.horde}').WaitingPrompt
ProcessingGeneration = import_module(name=f'horde.classes.{args.horde}').ProcessingGeneration
Worker = import_module(name=f'horde.classes.{args.horde}').Worker
PromptsIndex = import_module(name=f'horde.classes.{args.horde}').PromptsIndex
GenerationsIndex = import_module(name=f'horde.classes.{args.horde}').GenerationsIndex
User = import_module(name=f'horde.classes.{args.horde}').User
Database = import_module(name=f'horde.classes.{args.horde}').Database
# from .base import WaitingPrompt,ProcessingGeneration,Worker,PromptsIndex,GenerationsIndex,User,Database
db = Database(convert_flag=args.convert_flag)
waiting_prompts = PromptsIndex()
processing_generations = GenerationsIndex()
@@ -2,68 +2,56 @@ import json, os, sys
from uuid import uuid4
from datetime import datetime
import threading, time
from logger import logger
from .. import logger
from .. import thing_name,raw_thing_name
class WaitingPrompt:
def __init__(self, db, wps, pgs, prompt, user, params, **kwargs):
self._db = db
self.db = db
self._waiting_prompts = wps
self._processing_generations = pgs
self.prompt = prompt
self.user = user
self.params = params
self.n = params.pop('n', 1)
self.steps = params.pop('steps', 50)
# We assume more than 20 is not needed. But I'll re-evalute if anyone asks.
if self.n > 20:
logger.warning(f"User {self.user.get_unique_alias()} requested {self.n} gens per action. Reducing to 20...")
self.n = 20
self.width = params.get("width", 512)
self.height = params.get("height", 512)
# To avoid unnecessary calculations, we do it once here.
self.pixelsteps = self.width * self.height * self.steps
self.total_usage = round(self.pixelsteps * self.n / 1000000,2)
# The total amount of to pixelsteps requested.
self.total_usage = 0
self.extract_params(params)
self.id = str(uuid4())
# This is what we send to KoboldAI to the /generate/ API
self.gen_payload = params
self.gen_payload["prompt"] = prompt
# We always send only 1 iteration to KoboldAI
self.gen_payload["batch_size"] = 1
self.gen_payload["ddim_steps"] = self.steps
# The generations that have been created already
self.processing_gens = []
self.last_process_time = datetime.now()
self.servers = kwargs.get("servers", [])
self.workers = kwargs.get("workers", [])
# Prompt requests are removed after 1 mins of inactivity per n, to a max of 5 minutes
self.stale_time = 180 * self.n
if self.stale_time > 600:
self.stale_time = 600
# These are typically worker-specific so they will be defined in the specific class for this horde type
def extract_params(self, params):
self.n = params.pop('n', 1)
self.prepare_job_payload(params)
def prepare_job_payload(self, initial_dict = {}):
# This is what we send to the worker
self.gen_payload = initial_dict
def activate(self):
# We separate the activation from __init__ as often we want to check if there's a valid server for it
# Before we add it to the queue
'''We separate the activation from __init__ as often we want to check if there's a valid worker for it
Before we add it to the queue
'''
self._waiting_prompts.add_item(self)
logger.info(f"New prompt by {self.user.get_unique_alias()}: w:{self.width} * h:{self.height} * s:{self.steps} * n:{self.n} == {self.total_usage} Total MPs")
thread = threading.Thread(target=self.check_for_stale, args=())
thread.daemon = True
thread.start()
# The mps still queued to be generated for this WP
def get_queued_megapixelsteps(self):
return(round(self.pixelsteps * self.n/1000000,2))
def needs_gen(self):
if self.n > 0:
return(True)
return(False)
def start_generation(self, server):
def start_generation(self, worker):
if self.n <= 0:
return
new_gen = ProcessingGeneration(self, self._processing_generations, server)
new_gen = self.new_procgen(worker)
self.processing_gens.append(new_gen)
self.n -= 1
self.refresh()
@@ -73,6 +61,10 @@ class WaitingPrompt:
}
return(prompt_payload)
# Using this function so that I can extend it to have it grab the correct extended class
def new_procgen(self, worker):
return(ProcessingGeneration(self, self._processing_generations, worker))
def is_completed(self):
if self.needs_gen():
return(False)
@@ -93,59 +85,41 @@ class WaitingPrompt:
ret_dict["processing"] += 1
return(ret_dict)
# Should be overwritten
def get_queued_things(self):
return(0)
def get_status(self, lite = False):
ret_dict = self.count_processing_gens()
ret_dict["waiting"] = self.n
ret_dict["done"] = self.is_completed()
queue_pos, queued_mps, queued_n = self.get_own_queue_stats()
# We increment the priority by 1, because it starts at 0
# This means when all our requests are currently processing or done, with nothing else in the queue, we'll show queue position 0 which is appropriate.
ret_dict["queue_position"] = queue_pos + 1
active_servers = self._db.count_active_servers()
# If there's less requests than the number of active servers
# Then we need to adjust the parallelization accordingly
if queued_n < active_servers:
active_servers = queued_n
mpss = (self._db.stats.get_request_avg() / 1000000) * active_servers
# Is this is 0, it means one of two things:
# 1. This horde hasn't had any requests yet. So we'll initiate it to 1mpss
# 2. All gens for this WP are being currently processed, so we'll just set it to 1 to avoid a div by zero, but it's not used anyway as it will just divide 0/1
if mpss == 0:
mpss = 1
wait_time = queued_mps / mpss
# We add the expected running time of our processing gens
for procgen in self.processing_gens:
wait_time += procgen.get_expected_time_left()
ret_dict["wait_time"] = round(wait_time)
# Lite mode does not include the generations, to spare me download size
if not lite:
ret_dict["generations"] = []
for procgen in self.processing_gens:
if procgen.is_completed():
gen_dict = {
"img": procgen.generation,
"seed": procgen.seed,
"server_id": procgen.server.id,
"server_name": procgen.server.name,
}
ret_dict["generations"].append(gen_dict)
ret_dict["generations"].append(procgen.get_details())
return(ret_dict)
# Same as status, but without the images to avoid unnecessary size
def get_lite_status(self):
'''Same as get_status(), but without the images to avoid unnecessary size'''
ret_dict = self.get_status(True)
return(ret_dict)
# Get out position in the working prompts queue sorted by kudos
# If this gen is completed, we return (-1,-1) which represents this, to avoid doing operations.
def get_own_queue_stats(self):
'''Get out position in the working prompts queue sorted by kudos
If this gen is completed, we return (-1,-1) which represents this, to avoid doing operations.
'''
if self.needs_gen():
return(self._waiting_prompts.get_wp_queue_stats(self))
return(-1,0,0)
# Record that we received a requested generation and how much kudos it costs us
def record_usage(self, pixelsteps, kudos):
self.user.record_usage(pixelsteps, kudos)
def record_usage(self, thing, kudos):
'''Record that we received a requested generation and how much kudos it costs us
We use 'thing' here as we do not care what type of thing we're recording at this point
This avoids me having to extend this just to change a var name
'''
self.user.record_usage(thing, kudos)
self.refresh()
def check_for_stale(self):
@@ -173,29 +147,21 @@ class WaitingPrompt:
class ProcessingGeneration:
def __init__(self, owner, pgs, server):
def __init__(self, owner, pgs, worker):
self._processing_generations = pgs
self.id = str(uuid4())
self.owner = owner
self.server = server
self.generation = None
self.seed = None
self.kudos = 0
self.worker = worker
self.start_time = datetime.now()
self._processing_generations.add_item(self)
self.generation = None
def set_generation(self, generation, seed):
# This should be extended by every horde type
def set_generation(self, generation):
if self.is_completed():
return(0)
self.generation = generation
self.seed = seed
pixelsteps_per_sec = self.owner._db.stats.record_fulfilment(self.owner.pixelsteps, self.start_time)
self.kudos = self.owner._db.convert_pixelsteps_to_kudos(self.owner.pixelsteps)
self.server.record_contribution(self.owner.pixelsteps, self.kudos, pixelsteps_per_sec)
self.owner.record_usage(self.owner.pixelsteps, self.kudos)
logger.info(f"New Generation worth {self.kudos} kudos, delivered by server: {self.server.name}")
return(self.kudos)
return(0)
def is_completed(self):
if self.generation:
@@ -206,10 +172,14 @@ class ProcessingGeneration:
self._processing_generations.del_item(self)
del self
# This should be extended by every horde type
def get_seconds_needed(self):
return(0)
def get_expected_time_left(self):
if self.is_completed():
return(0)
seconds_needed = self.owner.pixelsteps / self.server.get_performance_average()
seconds_needed = self.get_seconds_needed()
seconds_elapsed = (datetime.now() - self.start_time).seconds
expected_time = seconds_needed - seconds_elapsed
# In case we run into a slow request
@@ -217,22 +187,30 @@ class ProcessingGeneration:
expected_time = 0
return(expected_time)
# This should be extended by every horde type
def get_details(self):
'''Returns a dictionary with details about this processing generation'''
ret_dict = {
"gen": procgen.generation,
"worker_id": procgen.worker.id,
"worker_name": procgen.worker.name,
}
return(ret_dict)
class KAIServer:
class Worker:
def __init__(self, db):
self._db = db
self.db = db
self.kudos_details = {
"generated": 0,
"uptime": 0,
}
self.last_reward_uptime = 0
# Every how many seconds does this server get a kudos reward
# Every how many seconds does this worker get a kudos reward
self.uptime_reward_threshold = 600
# Maintenance can be requested by the owner of the server (to allow them to not pick up more requests)
# Maintenance can be requested by the owner of the worker (to allow them to not pick up more requests)
self.maintenance = False
# Paused is set by the admins to prevent that server from seeing any more requests
# This can be used for stopping servers who misbhevave for example, without informing their owners
# Paused is set by the admins to prevent that worker from seeing any more requests
# This can be used for stopping workers who misbhevave for example, without informing their owners
self.paused = False
def create(self, user, name):
@@ -244,9 +222,10 @@ class KAIServer:
self.kudos = 0
self.performances = []
self.uptime = 0
self._db.register_new_server(self)
self.db.register_new_worker(self)
def check_in(self, max_pixels):
# This should be overwriten by each specific horde
def check_in(self):
if not self.is_stale():
self.uptime += (datetime.now() - self.last_check_in).seconds
# Every 10 minutes of uptime gets 100 kudos rewarded
@@ -254,15 +233,14 @@ class KAIServer:
kudos = 100
self.modify_kudos(kudos,'uptime')
self.user.record_uptime(kudos)
logger.debug(f"server '{self.name}' received {kudos} kudos for uptime of {self.uptime_reward_threshold} seconds.")
logger.debug(f"worker '{self.name}' received {kudos} kudos for uptime of {self.uptime_reward_threshold} seconds.")
self.last_reward_uptime = self.uptime
else:
# If the server comes back from being stale, we just reset their last_reward_uptime
# If the worker comes back from being stale, we just reset their last_reward_uptime
# So that they have to stay up at least 10 mins to get uptime kudos
self.last_reward_uptime = self.uptime
self.last_check_in = datetime.now()
self.max_pixels = max_pixels
logger.debug(f"Server {self.name} checked-in")
logger.debug(f"Worker {self.name} checked-in")
def get_human_readable_uptime(self):
if self.uptime < 60:
@@ -275,25 +253,32 @@ class KAIServer:
return(f"{round(self.uptime/60/60/24,2)} days")
def can_generate(self, waiting_prompt):
# takes as an argument a WaitingPrompt class and checks if this server is valid for generating it
# takes as an argument a WaitingPrompt class and checks if this worker is valid for generating it
is_matching = True
skipped_reason = None
# if thes server is paused, we return OK, but skip everything
if len(waiting_prompt.servers) >= 1 and self.id not in waiting_prompt.servers:
if self.is_stale():
# We don't consider stale workers in the request, so we don't need to report a reason
is_matching = False
skipped_reason = 'server_id'
if self.max_pixels < waiting_prompt.width * waiting_prompt.height:
# if thes worker is paused, we return OK, but skip everything
if len(waiting_prompt.workers) >= 1 and self.id not in waiting_prompt.workers:
is_matching = False
skipped_reason = 'max_pixels'
skipped_reason = 'worker_id'
return([is_matching,skipped_reason])
# We split it to its own function to make it extendable for specialized calculations
def convert_contribution(self, thing):
self.contributions = round(self.contributions + thing,2)
@logger.catch
def record_contribution(self, pixelsteps, kudos, pixelsteps_per_sec):
self.user.record_contributions(pixelsteps, kudos)
def record_contribution(self, thing, kudos, thing_per_sec):
'''We record the servers newest contribution
We do not need to know what type the contribution is, to avoid unnecessarily extending this method
'''
self.user.record_contributions(thing, kudos)
self.modify_kudos(kudos,'generated')
self.contributions = round(self.contributions + pixelsteps/1000000,2) # We store them as Megapixelsteps
self.convert_contribution(thing)
self.fulfilments += 1
self.performances.append(pixelsteps_per_sec)
self.performances.append(thing_per_sec)
if len(self.performances) > 20:
del self.performances[0]
@@ -305,13 +290,13 @@ class KAIServer:
if len(self.performances):
ret_num = sum(self.performances) / len(self.performances)
else:
# Always sending at least 1 pixelstep per second, to avoid divisions by zero
# Always sending at least 1 thing per second, to avoid divisions by zero
ret_num = 1
return(ret_num)
def get_performance(self):
if len(self.performances):
ret_str = f'{round(sum(self.performances) / len(self.performances),1)} pixelsteps per second'
ret_str = f'{round(sum(self.performances) / len(self.performances),1)} {raw_thing_name} per second'
else:
ret_str = f'No requests fulfilled yet'
return(ret_str)
@@ -320,17 +305,34 @@ class KAIServer:
try:
if (datetime.now() - self.last_check_in).seconds > 300:
return(True)
# If the last_check_in isn't set, it's a new server, so it's stale by default
# If the last_check_in isn't set, it's a new worker, so it's stale by default
except AttributeError:
return(True)
return(False)
# Should be extended by each specific horde
def get_details(self, is_privileged = False):
'''We display these in the workers list json'''
ret_dict = {
"name": self.name,
"id": self.id,
"requests_fulfilled": self.fulfilments,
"kudos_rewards": self.kudos,
"kudos_details": self.kudos_details,
"performance": self.get_performance(),
"uptime": self.uptime,
"maintenance_mode": self.maintenance,
}
if is_privileged:
ret_dict['paused'] = self.paused
return(ret_dict)
# Should be extended by each specific horde
@logger.catch
def serialize(self):
ret_dict = {
"oauth_id": self.user.oauth_id,
"name": self.name,
"max_pixels": self.max_pixels,
"contributions": self.contributions,
"fulfilments": self.fulfilments,
"kudos": self.kudos,
@@ -346,12 +348,9 @@ class KAIServer:
@logger.catch
def deserialize(self, saved_dict, convert_flag = None):
self.user = self._db.find_user_by_oauth_id(saved_dict["oauth_id"])
self.user = self.db.find_user_by_oauth_id(saved_dict["oauth_id"])
self.name = saved_dict["name"]
self.max_pixels = saved_dict["max_pixels"]
self.contributions = saved_dict["contributions"]
if convert_flag == 'pixelsteps':
self.contributions = round(self.contributions / 50,2)
self.fulfilments = saved_dict["fulfilments"]
self.kudos = saved_dict.get("kudos",0)
self.kudos_details = saved_dict.get("kudos_details",self.kudos_details)
@@ -361,7 +360,7 @@ class KAIServer:
self.uptime = saved_dict.get("uptime",0)
self.maintenance = saved_dict.get("maintenance",False)
self.paused = saved_dict.get("paused",False)
self._db.servers[self.name] = self
self.db.workers[self.name] = self
class Index:
@@ -369,14 +368,12 @@ class Index:
self._index = {}
def add_item(self, item):
logger.debug(item)
self._index[item.id] = item
def get_item(self, uuid):
return(self._index.get(uuid))
def del_item(self, item):
logger.debug(item)
del self._index[item.id]
def get_all(self):
@@ -405,15 +402,9 @@ class PromptsIndex(Index):
def count_totals(self):
ret_dict = {
"queued_requests": 0,
# mps == Megapixelsteps
"queued_megapixelsteps": 0,
}
for wp in self._index.values():
ret_dict["queued_requests"] += wp.n
if wp.n > 0:
ret_dict["queued_megapixelsteps"] += wp.pixelsteps / 1000000
# We round the end result to avoid to many decimals
ret_dict["queued_megapixelsteps"] = round(ret_dict["queued_megapixelsteps"],2)
ret_dict["queued_requests"] += wp.n + wp.count_processing_gens()["processing"]
return(ret_dict)
def get_waiting_wp_by_kudos(self):
@@ -425,18 +416,18 @@ class PromptsIndex(Index):
return(final_wp_list)
# Returns the queue position of the provided WP based on kudos
# Also returns the amount of mps until the wp is generated
# Also returns the amount of things until the wp is generated
# Also returns the amount of different gens queued
def get_wp_queue_stats(self, wp):
mps_ahead_in_queue = 0
things_ahead_in_queue = 0
n_ahead_in_queue = 0
priority_sorted_list = self.get_waiting_wp_by_kudos()
for iter in range(len(priority_sorted_list)):
mps_ahead_in_queue += priority_sorted_list[iter].get_queued_megapixelsteps()
things_ahead_in_queue += priority_sorted_list[iter].get_queued_things()
n_ahead_in_queue += priority_sorted_list[iter].n
if priority_sorted_list[iter] == wp:
mps_ahead_in_queue = round(mps_ahead_in_queue,2)
return(iter, mps_ahead_in_queue, n_ahead_in_queue)
things_ahead_in_queue = round(things_ahead_in_queue,2)
return(iter, things_ahead_in_queue, n_ahead_in_queue)
# -1 means the WP is done and not in the queue
return(-1,0,0)
@@ -447,7 +438,7 @@ class GenerationsIndex(Index):
class User:
def __init__(self, db):
self._db = db
self.db = db
self.kudos = 0
self.kudos_details = {
"accumulated": 0,
@@ -467,11 +458,11 @@ class User:
self.last_active = datetime.now()
self.id = 0
self.contributions = {
"megapixelsteps": 0,
thing_name: 0,
"fulfillments": 0
}
self.usage = {
"megapixelsteps": 0,
thing_name: 0,
"requests": 0
}
# We allow anonymous users more leeway for the max amount of concurrent requests
@@ -485,13 +476,13 @@ class User:
self.invite_id = invite_id
self.creation_date = datetime.now()
self.last_active = datetime.now()
self.id = self._db.register_new_user(self)
self.id = self.db.register_new_user(self)
self.contributions = {
"megapixelsteps": 0,
thing_name: 0,
"fulfillments": 0
}
self.usage = {
"megapixelsteps": 0,
thing_name: 0,
"requests": 0
}
@@ -504,13 +495,11 @@ class User:
def get_unique_alias(self):
return(f"{self.username}#{self.id}")
def record_usage(self, pixelsteps, kudos):
self.usage["megapixelsteps"] = round(self.usage["megapixelsteps"] + (pixelsteps * self.usage_multiplier / 1000000),2)
def record_usage(self, kudos):
self.usage["requests"] += 1
self.modify_kudos(-kudos,"accumulated")
def record_contributions(self, pixelsteps, kudos):
self.contributions["megapixelsteps"] = round(self.contributions["megapixelsteps"] + pixelsteps/1000000,2)
def record_contributions(self, kudos):
self.contributions["fulfillments"] += 1
self.modify_kudos(kudos,"accumulated")
@@ -522,6 +511,18 @@ class User:
self.kudos = round(self.kudos + kudos, 2)
self.kudos_details[action] = round(self.kudos_details.get(action,0) + kudos, 2)
def get_details(self):
ret_dict = {
"username": self.get_unique_alias(),
"id": self.id,
"kudos": self.kudos,
"kudos_details": self.kudos_details,
"usage": self.usage,
"contributions": self.contributions,
"concurrency": self.concurrency,
}
return(ret_dict)
@logger.catch
def serialize(self):
@@ -557,12 +558,6 @@ class User:
self.usage_multiplier = saved_dict.get("usage_multiplier", 1.0)
if self.api_key == '0000000000':
self.concurrency = 200
if convert_flag == 'pixelsteps':
# I average to 25 steps, to convert pixels to pixelsteps, since I wasn't tracking it until now
self.contributions['megapixelsteps'] = round(self.contributions['pixels'] / 50,2)
del self.contributions['pixels']
self.usage['megapixelsteps'] = round(self.usage['pixels'] / 50,2)
del self.usage['pixels']
self.creation_date = datetime.strptime(saved_dict["creation_date"],"%Y-%m-%d %H:%M:%S")
self.last_active = datetime.strptime(saved_dict["last_active"],"%Y-%m-%d %H:%M:%S")
@@ -570,46 +565,45 @@ class User:
class Stats:
def __init__(self, db, convert_flag = None, interval = 60):
self.db = db
self.server_performances = []
self.worker_performances = []
self.fulfillments = []
self.interval = interval
self.last_pruning = datetime.now()
def record_fulfilment(self, pixelsteps, starting_time):
def record_fulfilment(self, things, starting_time):
seconds_taken = (datetime.now() - starting_time).seconds
if seconds_taken == 0:
pixelsteps_per_sec = 1
things_per_sec = 1
else:
pixelsteps_per_sec = round(pixelsteps / seconds_taken,1)
if len(self.server_performances) >= 10:
del self.server_performances[0]
self.server_performances.append(pixelsteps_per_sec)
things_per_sec = round(things / seconds_taken,1)
if len(self.worker_performances) >= 10:
del self.worker_performances[0]
self.worker_performances.append(things_per_sec)
fulfillment_dict = {
"pixelsteps": pixelsteps,
raw_thing_name: things,
"start_time": starting_time,
"deliver_time": datetime.now(),
}
self.fulfillments.append(fulfillment_dict)
return(pixelsteps_per_sec)
return(things_per_sec)
def get_megapixelsteps_per_min(self):
total_pixelsteps = 0
def get_things_per_min(self):
total_things = 0
pruned_array = []
for fulfillment in self.fulfillments:
if (datetime.now() - fulfillment["deliver_time"]).seconds <= 60:
pruned_array.append(fulfillment)
total_pixelsteps += fulfillment["pixelsteps"]
total_things += fulfillment[raw_thing_name]
if (datetime.now() - self.last_pruning).seconds > self.interval:
self.last_pruning = datetime.now()
self.fulfillments = pruned_array
logger.debug("Pruned fulfillments")
megapixelsteps_per_min = round(total_pixelsteps / 1000000,2)
return(megapixelsteps_per_min)
return(total_things)
def get_request_avg(self):
if len(self.server_performances) == 0:
if len(self.worker_performances) == 0:
return(0)
avg = sum(self.server_performances) / len(self.server_performances)
avg = sum(self.worker_performances) / len(self.worker_performances)
return(round(avg,1))
@logger.catch
@@ -617,14 +611,13 @@ class Stats:
serialized_fulfillments = []
for fulfillment in self.fulfillments.copy():
json_fulfillment = {
"pixelsteps": fulfillment["pixelsteps"],
raw_thing_name: fulfillment[raw_thing_name],
"start_time": fulfillment["start_time"].strftime("%Y-%m-%d %H:%M:%S"),
"deliver_time": fulfillment["deliver_time"].strftime("%Y-%m-%d %H:%M:%S"),
}
serialized_fulfillments.append(json_fulfillment)
ret_dict = {
"server_performances": self.server_performances,
"model_mulitpliers": self.model_mulitpliers,
"worker_performances": self.worker_performances,
"fulfillments": serialized_fulfillments,
}
return(ret_dict)
@@ -632,19 +625,18 @@ class Stats:
@logger.catch
def deserialize(self, saved_dict, convert_flag = None):
# Convert old key
if "fulfilment_times" in saved_dict:
self.server_performances = saved_dict["fulfilment_times"]
if "server_performances" in saved_dict:
self.worker_performances = saved_dict["server_performances"]
else:
self.server_performances = saved_dict["server_performances"]
self.worker_performances = saved_dict["worker_performances"]
deserialized_fulfillments = []
for fulfillment in saved_dict.get("fulfillments", []):
class_fulfillment = {
"pixelsteps": fulfillment["pixelsteps"],
raw_thing_name: fulfillment[raw_thing_name],
"start_time": datetime.strptime(fulfillment["start_time"],"%Y-%m-%d %H:%M:%S"),
"deliver_time":datetime.strptime(fulfillment["deliver_time"],"%Y-%m-%d %H:%M:%S"),
}
deserialized_fulfillments.append(class_fulfillment)
self.model_mulitpliers = saved_dict["model_mulitpliers"]
self.fulfillments = deserialized_fulfillments
class Database:
@@ -652,11 +644,11 @@ class Database:
self.interval = interval
self.ALLOW_ANONYMOUS = True
# This is used for synchronous generations
self.SERVERS_FILE = "db/servers.json"
self.servers = {}
self.WORKERS_FILE = "db/workers.json"
self.workers = {}
# Other miscellaneous statistics
self.STATS_FILE = "db/stats.json"
self.stats = Stats(self)
self.stats = self.new_stats()
self.USERS_FILE = "db/users.json"
self.users = {}
# Increments any time a new user is added
@@ -672,7 +664,7 @@ class Database:
if not user_dict:
logger.error("Found null user on db load. Bypassing")
continue
new_user = User(self)
new_user = self.new_user()
new_user.deserialize(user_dict,convert_flag)
self.users[new_user.oauth_id] = new_user
if new_user.id > self.last_user_id:
@@ -682,16 +674,16 @@ class Database:
self.anon = User(self)
self.anon.create_anon()
self.users[self.anon.oauth_id] = self.anon
if os.path.isfile(self.SERVERS_FILE):
with open(self.SERVERS_FILE) as db:
serialized_servers = json.load(db)
for server_dict in serialized_servers:
if not server_dict:
logger.error("Found null server on db load. Bypassing")
if os.path.isfile(self.WORKERS_FILE):
with open(self.WORKERS_FILE) as db:
serialized_workers = json.load(db)
for worker_dict in serialized_workers:
if not worker_dict:
logger.error("Found null worker on db load. Bypassing")
continue
new_server = KAIServer(self)
new_server.deserialize(server_dict,convert_flag)
self.servers[new_server.name] = new_server
new_worker = self.new_worker()
new_worker.deserialize(worker_dict,convert_flag)
self.workers[new_worker.name] = new_worker
if os.path.isfile(self.STATS_FILE):
with open(self.STATS_FILE) as stats_db:
self.stats.deserialize(json.load(stats_db),convert_flag)
@@ -705,6 +697,15 @@ class Database:
thread.start()
logger.init_ok(f"Database Load", status="Completed")
# I don't know if I'm doing this right, but I'm using these so that I can extend them from the extended DB
# So that it will grab the extended classes from each horde type, and not the internal classes in this package
def new_worker(self):
return(Worker(self))
def new_user(self):
return(User(self))
def new_stats(self):
return(Stats(self))
def write_files(self):
logger.init_ok("Database Store Thread", status="Started")
while True:
@@ -714,14 +715,14 @@ class Database:
def write_files_to_disk(self):
if not os.path.exists('db'):
os.mkdir('db')
server_serialized_list = []
worker_serialized_list = []
logger.debug("Saving DB")
for server in self.servers.copy().values():
# We don't store data for anon servers
if server.user == self.anon: continue
server_serialized_list.append(server.serialize())
with open(self.SERVERS_FILE, 'w') as db:
json.dump(server_serialized_list,db)
for worker in self.workers.copy().values():
# We don't store data for anon workers
if worker.user == self.anon: continue
worker_serialized_list.append(worker.serialize())
with open(self.WORKERS_FILE, 'w') as db:
json.dump(worker_serialized_list,db)
with open(self.STATS_FILE, 'w') as db:
json.dump(self.stats.serialize(),db)
user_serialized_list = []
@@ -735,35 +736,35 @@ class Database:
top_contributor = None
user = None
for user in self.users.values():
if user.contributions['megapixelsteps'] > top_contribution and user != self.anon:
if user.contributions[thing_name] > top_contribution and user != self.anon:
top_contributor = user
top_contribution = user.contributions['megapixelsteps']
top_contribution = user.contributions[thing_name]
return(top_contributor)
def get_top_server(self):
top_server = None
top_server_contribution = 0
for server in self.servers:
if self.servers[server].contributions > top_server_contribution:
top_server = self.servers[server]
top_server_contribution = self.servers[server].contributions
return(top_server)
def get_top_worker(self):
top_worker = None
top_worker_contribution = 0
for worker in self.workers:
if self.workers[worker].contributions > top_worker_contribution:
top_worker = self.workers[worker]
top_worker_contribution = self.workers[worker].contributions
return(top_worker)
def count_active_servers(self):
def count_active_workers(self):
count = 0
for server in self.servers.values():
if not server.is_stale():
for worker in self.workers.values():
if not worker.is_stale():
count += 1
return(count)
def get_total_usage(self):
totals = {
"megapixelsteps": 0,
thing_name: 0,
"fulfilments": 0,
}
for server in self.servers.values():
totals["megapixelsteps"] += server.contributions
totals["fulfilments"] += server.fulfilments
for worker in self.workers.values():
totals[thing_name] += worker.contributions
totals["fulfilments"] += worker.fulfilments
return(totals)
@@ -773,16 +774,16 @@ class Database:
logger.info(f'New user created: {user.username}#{self.last_user_id}')
return(self.last_user_id)
def register_new_server(self, server):
self.servers[server.name] = server
logger.info(f'New server checked-in: {server.name} by {server.user.get_unique_alias()}')
def register_new_worker(self, worker):
self.workers[worker.name] = worker
logger.info(f'New worker checked-in: {worker.name} by {worker.user.get_unique_alias()}')
def find_user_by_oauth_id(self,oauth_id):
if oauth_id == 'anon' and not self.ALLOW_ANONYMOUS:
return(None)
return(self.users.get(oauth_id))
def find_user_by_username(self, id):
def find_user_by_username(self, username):
for user in self.users.values():
ulist = username.split('#')
# This approach handles someone cheekily putting # in their username
@@ -809,13 +810,13 @@ class Database:
return(user)
return(None)
def find_server_by_name(self,server_name):
return(self.servers.get(server_name))
def find_worker_by_name(self,worker_name):
return(self.workers.get(worker_name))
def find_server_by_id(self,server_id):
for server in self.servers.values():
if server.id == server_id:
return(server)
def find_worker_by_id(self,worker_id):
for worker in self.workers.values():
if worker.id == worker_id:
return(worker)
return(None)
def transfer_kudos(self, source_user, dest_user, amount):
@@ -845,8 +846,10 @@ class Database:
kudos = self.transfer_kudos_to_username(source_user, dest_username, amount)
return(kudos)
def convert_pixelsteps_to_kudos(self, pixelsteps):
# Should be overriden
def convert_things_to_kudos(self, things):
# The baseline for a standard generation of 512x512, 50 steps is 10 kudos
kudos = round(pixelsteps / (512*512*5),2)
kudos = round(things,2)
# logger.info([pixels,multiplier,kudos])
return(kudos)
+13
View File
@@ -0,0 +1,13 @@
class Maintenance:
active = False
def activate(self):
self.active = True
def disable(self):
self.active = False
def toggle(self,value):
self.active = value
+205
View File
@@ -0,0 +1,205 @@
from ..base import *
class WaitingPrompt(WaitingPrompt):
def extract_params(self, params):
self.n = params.pop('n', 1)
self.steps = params.pop('steps', 50)
# We assume more than 20 is not needed. But I'll re-evalute if anyone asks.
if self.n > 20:
logger.warning(f"User {self.user.get_unique_alias()} requested {self.n} gens per action. Reducing to 20...")
self.n = 20
self.width = params.get("width", 512)
self.height = params.get("height", 512)
# To avoid unnecessary calculations, we do it once here.
self.pixelsteps = self.width * self.height * self.steps
# The total amount of to pixelsteps requested.
self.total_usage = round(self.pixelsteps * self.n / 1000000,2)
self.prepare_job_payload(params)
def prepare_job_payload(self, initial_dict = {}):
# This is what we send to KoboldAI to the /generate/ API
self.gen_payload = initial_dict
self.gen_payload["prompt"] = self.prompt
# We always send only 1 iteration to KoboldAI
self.gen_payload["batch_size"] = 1
self.gen_payload["ddim_steps"] = self.steps
def activate(self):
# We separate the activation from __init__ as often we want to check if there's a valid worker for it
# Before we add it to the queue
super().activate()
logger.info(f"New prompt by {self.user.get_unique_alias()}: w:{self.width} * h:{self.height} * s:{self.steps} * n:{self.n} == {self.total_usage} Total MPs")
def new_procgen(self, worker):
return(ProcessingGeneration(self, self._processing_generations, worker))
def get_queued_things(self):
'''The mps still queued to be generated for this WP
For the Stable Horde, things == megapixelsteps
'''
return(round(self.pixelsteps * self.n/1000000,2))
def get_status(self, lite = False):
ret_dict = super().get_status(lite)
queue_pos, queued_mps, queued_n = self.get_own_queue_stats()
# We increment the priority by 1, because it starts at 0
# This means when all our requests are currently processing or done, with nothing else in the queue, we'll show queue position 0 which is appropriate.
ret_dict["queue_position"] = queue_pos + 1
active_workers = self.db.count_active_workers()
# If there's less requests than the number of active workers
# Then we need to adjust the parallelization accordingly
if queued_n < active_workers:
active_workers = queued_n
mpss = (self.db.stats.get_request_avg() / 1000000) * active_workers
# Is this is 0, it means one of two things:
# 1. This horde hasn't had any requests yet. So we'll initiate it to 1mpss
# 2. All gens for this WP are being currently processed, so we'll just set it to 1 to avoid a div by zero, but it's not used anyway as it will just divide 0/1
if mpss == 0:
mpss = 1
wait_time = queued_mps / mpss
# We add the expected running time of our processing gens
for procgen in self.processing_gens:
wait_time += procgen.get_expected_time_left()
ret_dict["wait_time"] = round(wait_time)
return(ret_dict)
class ProcessingGeneration(ProcessingGeneration):
def set_generation(self, generation, seed):
if self.is_completed():
return(0)
self.generation = generation
self.seed = seed
pixelsteps_per_sec = self.owner.db.stats.record_fulfilment(self.owner.pixelsteps, self.start_time)
self.kudos = self.owner.db.convert_things_to_kudos(self.owner.pixelsteps)
self.worker.record_contribution(self.owner.pixelsteps, self.kudos, pixelsteps_per_sec)
self.owner.record_usage(self.owner.pixelsteps, self.kudos)
logger.info(f"New Generation worth {self.kudos} kudos, delivered by worker: {self.worker.name}")
return(self.kudos)
def get_seconds_needed(self):
return(self.owner.pixelsteps / self.worker.get_performance_average())
def get_details(self):
'''Returns a dictionary with details about this processing generation'''
ret_dict = {
"img": self.generation,
"seed": self.seed,
"worker_id": self.worker.id,
"worker_name": self.worker.name,
}
return(ret_dict)
class Worker(Worker):
def check_in(self, max_pixels):
if not self.is_stale():
self.uptime += (datetime.now() - self.last_check_in).seconds
# Every 10 minutes of uptime gets 100 kudos rewarded
if self.uptime - self.last_reward_uptime > self.uptime_reward_threshold:
kudos = 100
self.modify_kudos(kudos,'uptime')
self.user.record_uptime(kudos)
logger.debug(f"worker '{self.name}' received {kudos} kudos for uptime of {self.uptime_reward_threshold} seconds.")
self.last_reward_uptime = self.uptime
else:
# If the worker comes back from being stale, we just reset their last_reward_uptime
# So that they have to stay up at least 10 mins to get uptime kudos
self.last_reward_uptime = self.uptime
self.last_check_in = datetime.now()
self.max_pixels = max_pixels
logger.debug(f"Worker {self.name} checked-in, offering {self.max_pixels} max pixels")
def can_generate(self, waiting_prompt):
can_generate = super().can_generate(waiting_prompt)
is_matching = can_generate[0]
skipped_reason = can_generate[1]
if self.max_pixels < waiting_prompt.width * waiting_prompt.height:
is_matching = False
skipped_reason = 'max_pixels'
return([is_matching,skipped_reason])
# We split it to its own function to make it extendable
def convert_contribution(self,pixelsteps):
self.contributions = round(self.contributions + pixelsteps/1000000,2)
def get_details(self, is_privileged = False):
ret_dict = super().get_details(is_privileged)
ret_dict["max_pixels"] = self.max_pixels
ret_dict["megapixelsteps_generated"] = self.contributions
return(ret_dict)
def get_performance(self):
if len(self.performances):
ret_str = f'{round(sum(self.performances) / len(self.performances) / 1000000,1)} {thing_name} per second'
else:
ret_str = f'No requests fulfilled yet'
return(ret_str)
@logger.catch
def serialize(self):
ret_dict = super().serialize()
ret_dict["max_pixels"] = self.max_pixels
return(ret_dict)
@logger.catch
def deserialize(self, saved_dict, convert_flag = None):
super().deserialize(saved_dict, convert_flag)
self.max_pixels = saved_dict["max_pixels"]
if convert_flag == 'pixelsteps':
self.contributions = round(self.contributions / 50,2)
class PromptsIndex(PromptsIndex):
def count_totals(self):
ret_dict = {
"queued_requests": 0,
# mps == Megapixelsteps
"queued_megapixelsteps": 0,
}
for wp in self._index.values():
current_wp_queue = wp.n + wp.count_processing_gens()["processing"]
ret_dict["queued_requests"] += current_wp_queue
if current_wp_queue > 0:
ret_dict["queued_megapixelsteps"] += wp.pixelsteps * current_wp_queue / 1000000
# We round the end result to avoid to many decimals
ret_dict["queued_megapixelsteps"] = round(ret_dict["queued_megapixelsteps"],2)
return(ret_dict)
class User(User):
def record_usage(self, pixelsteps, kudos):
super().record_usage(kudos)
self.usage[thing_name] = round(self.usage[thing_name] + (pixelsteps * self.usage_multiplier / 1000000),2)
def record_contributions(self, pixelsteps, kudos):
super().record_contributions(kudos)
self.contributions[thing_name] = round(self.contributions[thing_name] + pixelsteps/1000000,2)
class Stats(Stats):
def get_things_per_min(self):
pixelsteps_per_min = super().get_things_per_min()
megapixelsteps_per_min = round(pixelsteps_per_min / 1000000,2)
return(megapixelsteps_per_min)
class Database(Database):
def convert_things_to_kudos(self, pixelsteps):
# The baseline for a standard generation of 512x512, 50 steps is 10 kudos
kudos = round(pixelsteps / (512*512*5),2)
# logger.info([pixels,multiplier,kudos])
return(kudos)
def new_worker(self):
return(Worker(self))
def new_user(self):
return(User(self))
def new_stats(self):
return(Stats(self))
+6
View File
@@ -0,0 +1,6 @@
from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix
HORDE = Flask(__name__)
HORDE.wsgi_app = ProxyFix(HORDE.wsgi_app, x_for=1)
+21
View File
@@ -0,0 +1,21 @@
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from .flask import HORDE
# Very basic DOS prevention
try:
limiter = Limiter(
HORDE,
key_func=get_remote_address,
storage_uri="redis://localhost:6379/1",
# storage_options={"connect_timeout": 30},
strategy="fixed-window", # or "moving-window"
default_limits=["90 per minute"]
)
# Allow local workatation run
except:
limiter = Limiter(
HORDE,
key_func=get_remote_address,
default_limits=["90 per minute"]
)
+5
View File
@@ -97,3 +97,8 @@ config = {
],
}
logger.configure(**config)
logger.disable("__main__")
logger.warning("disabled")
logger.enable("")
logger.enable(None)
+221
View File
@@ -0,0 +1,221 @@
from flask import render_template, redirect, url_for, request
import random, time, os, oauthlib, secrets, logging
from flask_dance.contrib.google import google
from flask_dance.contrib.discord import discord
from flask_dance.contrib.github import github
from markdown import markdown
from uuid import uuid4
from . import logger, maintenance, HORDE
from .classes import db
from .classes import waiting_prompts,User
import bleach
dance_return_to = '/'
@logger.catch
@HORDE.route('/')
def index():
with open('index.md') as index_file:
index = index_file.read()
top_contributor = db.get_top_contributor()
top_worker = db.get_top_worker()
align_image = 0
big_image = align_image
while big_image == align_image:
big_image = random.randint(1, 5)
if not top_contributor or not top_worker:
top_contributors = f'\n<img src="https://github.com/db0/Stable-Horde/blob/master/img/{big_image}.png?raw=true" width="800" />'
else:
top_contributors = f"""\n## Top Contributors
These are the people and workers who have contributed most to this horde.
### Users
This is the person whose worker(s) have generated the most pixels for the horde.
#### {top_contributor.get_unique_alias()}
* {round(top_contributor.contributions['megapixelsteps'] / 1000,2)} Gigapixelsteps generated.
* {top_contributor.contributions['fulfillments']} requests fulfilled.
### Workers
This is the worker which has generated the most pixels for the horde.
#### {top_worker.name}
* {round(top_worker.contributions/1000,2)} Gigapixelsteps generated.
* {top_worker.fulfilments} request fulfillments.
* {top_worker.get_human_readable_uptime()} uptime.
"""
policies = """
## Policies
[Privacy Policy](/privacy)
[Terms of Service](/terms)"""
totals = db.get_total_usage()
wp_totals = waiting_prompts.count_totals()
findex = index.format(
stable_image = align_image,
avg_performance= round(db.stats.get_request_avg() / 1000000,2),
total_pixels = round(totals["megapixelsteps"] / 1000,2),
total_fulfillments = totals["fulfilments"],
active_workers = db.count_active_workers(),
total_queue = wp_totals["queued_requests"],
total_mps = wp_totals["queued_megapixelsteps"],
maintenance_mode = maintenance.active,
)
head = """<head>
<title>Stable Horde</title>
<meta name="google-site-verification" content="pmLKyCEPKM5csKT9mW1ZbGLu2TX_wD0S5FCxWlmg_iI" />
</head>
"""
return(head + markdown(findex + top_contributors + policies))
@logger.catch
def get_oauth_id():
google_data = None
discord_data = None
github_data = None
authorized = False
if google.authorized:
google_user_info_endpoint = '/oauth2/v2/userinfo'
try:
google_data = google.get(google_user_info_endpoint).json()
authorized = True
except oauthlib.oauth2.rfc6749.errors.TokenExpiredError:
pass
if not authorized and discord.authorized:
discord_info_endpoint = '/api/users/@me'
try:
discord_data = discord.get(discord_info_endpoint).json()
authorized = True
except oauthlib.oauth2.rfc6749.errors.TokenExpiredError:
pass
if not authorized and github.authorized:
github_info_endpoint = '/user'
try:
github_data = github.get(github_info_endpoint).json()
authorized = True
except oauthlib.oauth2.rfc6749.errors.TokenExpiredError:
pass
oauth_id = None
if google_data:
oauth_id = f'g_{google_data["id"]}'
elif discord_data:
oauth_id = f'd_{discord_data["id"]}'
elif github_data:
oauth_id = f'gh_{github_data["id"]}'
return(oauth_id)
@logger.catch
@HORDE.route('/register', methods=['GET', 'POST'])
def register():
api_key = None
user = None
welcome = 'Welcome'
username = ''
pseudonymous = False
oauth_id = get_oauth_id()
if oauth_id:
user = db.find_user_by_oauth_id(oauth_id)
if user:
username = user.username
if request.method == 'POST':
api_key = secrets.token_urlsafe(16)
if user:
username = bleach.clean(request.form['username'])
user.username = username
user.api_key = api_key
else:
# Triggered when the user created a username without logging in
if not oauth_id:
oauth_id = str(uuid4())
pseudonymous = True
user = User(db)
user.create(request.form['username'], oauth_id, api_key, None)
username = bleach.clean(request.form['username'])
if user:
welcome = f"Welcome back {user.get_unique_alias()}"
return render_template('register.html',
page_title="Join the Stable Horde!",
welcome=welcome,
user=user,
api_key=api_key,
username=username,
pseudonymous=pseudonymous,
oauth_id=oauth_id)
@logger.catch
@HORDE.route('/transfer', methods=['GET', 'POST'])
def transfer():
src_api_key = None
src_user = None
dest_username = None
kudos = None
error = None
welcome = 'Welcome'
oauth_id = get_oauth_id()
if oauth_id:
src_user = db.find_user_by_oauth_id(oauth_id)
if not src_user:
# This probably means the user was deleted
oauth_id = None
if request.method == 'POST':
dest_username = request.form['username']
amount = request.form['amount']
if not amount.isnumeric():
kudos = 0
error = "Please enter a number in the kudos field"
# Triggered when the user submited without logging in
elif src_user:
ret = db.transfer_kudos_to_username(src_user,dest_username,int(amount))
kudos = ret[0]
error = ret[1]
else:
ret = db.transfer_kudos_from_apikey_to_username(request.form['src_api_key'],dest_username,int(amount))
kudos = ret[0]
error = ret[1]
if src_user:
welcome = f"Welcome back {src_user.get_unique_alias()}. You have {src_user.kudos} kudos remaining"
return render_template('transfer_kudos.html',
page_title="Kudos Transfer",
welcome=welcome,
kudos=kudos,
error=error,
dest_username=dest_username,
oauth_id=oauth_id)
@HORDE.route('/google/<return_to>')
def google_login(return_to):
global dance_return_to
dance_return_to = '/' + return_to
return redirect(url_for('google.login'))
@HORDE.route('/discord/<return_to>')
def discord_login(return_to):
global dance_return_to
dance_return_to = '/' + return_to
return redirect(url_for('discord.login'))
@HORDE.route('/github/<return_to>')
def github_login(return_to):
global dance_return_to
dance_return_to = '/' + return_to
return redirect(url_for('github.login'))
@HORDE.route('/finish_dance')
def finish_dance():
global dance_return_to
redirect_url = dance_return_to
dance_return_to = '/'
return redirect(redirect_url)
@HORDE.route('/privacy')
def privacy():
return render_template('privacy_policy.html')
@HORDE.route('/terms')
def terms():
return render_template('terms_of_service.html')
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+9 -4
View File
@@ -7,10 +7,10 @@ Also check out our sister project: [KoboldAI Horde](https://koboldai.net)
## Stats
* Average Recent Performance: {avg_performance} Megapixelsteps per second
* Total GP⋅s generated: {total_pixels} Gigapixelsteps
* Total GPS generated: {total_pixels} Gigapixelsteps
* Total requests fulfilled: {total_fulfillments}
* Active [Servers](/api/v1/servers): {active_servers}
* Current Request Queue: {total_queue}
* Active [Workers](/api/v2/workers): {active_workers}
* Queue: {total_queue} requests for a total of {total_mps} Megapixelsteps
## Usage
@@ -40,6 +40,10 @@ You can make a copy of `cliRequestData_template.py` into `cliRequestData.py` and
We provide a [Godot Engine plugin](https://github.com/db0/Stable-Horde-Client-Addon) to integrate Stable Horde image generation into your games.
## REST API
[Full Documentation](/api)
## Services
* [Register New Account](/register)
@@ -47,7 +51,8 @@ We provide a [Godot Engine plugin](https://github.com/db0/Stable-Horde-Client-Ad
## Community
Join us on [Discord](https://discord.gg/3DxrhksKzn)
* Join us on [Discord](https://discord.gg/3DxrhksKzn)
* Support the development of the Stable Horde on [Patreon](https://www.patreon.com/db0) or [Github](https://github.com/db0)
## Credits
+4 -2
View File
@@ -1,5 +1,6 @@
werkzeug==2.1.2
Flask
flask_restful
flask-restx
flask_limiter
waitress
requests >= 2.27
@@ -7,4 +8,5 @@ Markdown
flask-dance[sqla]
blinker
python-dotenv
loguru
loguru
bleach
+6 -791
View File
@@ -1,805 +1,20 @@
from flask import Flask, render_template, redirect, url_for, request, abort
from flask_restful import Resource, reqparse, Api
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from werkzeug.middleware.proxy_fix import ProxyFix
from flask_dance.contrib.google import make_google_blueprint, google
from flask_dance.contrib.discord import make_discord_blueprint, discord
from flask_dance.contrib.github import make_github_blueprint, github
import requests, random, time, os, oauthlib, secrets, argparse, logging
from enum import Enum
from markdown import markdown
from dotenv import load_dotenv
from uuid import uuid4
from werkzeug.middleware.proxy_fix import ProxyFix
from server_classes import WaitingPrompt,ProcessingGeneration,KAIServer,PromptsIndex,GenerationsIndex,User,Database
from logger import logger, set_logger_verbosity, quiesce_logger
class ServerErrors(Enum):
WRONG_CREDENTIALS = 0
INVALID_PROCGEN = 1
DUPLICATE_GEN = 2
TOO_MANY_PROMPTS = 3
EMPTY_PROMPT = 4
INVALID_API_KEY = 5
INVALID_SIZE = 6
NO_PROXY = 7
TOO_MANY_STEPS = 8
NOT_ADMIN = 9
MAINTENANCE_MODE = 10
NOT_OWNER = 11
REST_API = Flask(__name__)
REST_API.wsgi_app = ProxyFix(REST_API.wsgi_app, x_for=1)
# Very basic DOS prevention
try:
limiter = Limiter(
REST_API,
key_func=get_remote_address,
storage_uri="redis://localhost:6379/1",
# storage_options={"connect_timeout": 30},
strategy="fixed-window", # or "moving-window"
default_limits=["90 per minute"]
)
# Allow local workatation run
except:
limiter = Limiter(
REST_API,
key_func=get_remote_address,
default_limits=["90 per minute"]
)
api = Api(REST_API)
dance_return_to = '/'
maintenance_mode = False
allow_direct_connections = False
load_dotenv()
@logger.catch
def get_error(error, **kwargs):
if error == ServerErrors.INVALID_API_KEY:
logger.warning(f'Invalid API Key sent for {kwargs["subject"]}.')
return(f'No user matching sent API Key. Have you remembered to register at https://stablehorde.net/register ?')
if error == ServerErrors.WRONG_CREDENTIALS:
logger.warning(f'User "{kwargs["username"]}" sent wrong credentials for utilizing instance {kwargs["kai_instance"]}')
return(f'wrong credentials for utilizing instance {kwargs["kai_instance"]}')
if error == ServerErrors.INVALID_PROCGEN:
logger.warning(f'Server attempted to provide generation for {kwargs["id"]} but it did not exist')
return(f'Processing Generation with ID {kwargs["id"]} does not exist')
if error == ServerErrors.DUPLICATE_GEN:
logger.warning(f'Server attempted to provide duplicate generation for {kwargs["id"]} ')
return(f'Processing Generation with ID {kwargs["id"]} already submitted')
if error == ServerErrors.TOO_MANY_PROMPTS:
logger.warning(f'User "{kwargs["username"]}" has already requested too many parallel requests ({kwargs["wp_count"]}). Aborting!')
return(f"Parallel requests exceeded user limit ({kwargs['wp_count']}). Please try again later or request to increase your concurrency.")
if error == ServerErrors.EMPTY_PROMPT:
logger.warning(f'User "{kwargs["username"]}" sent an empty prompt. Aborting!')
return("You cannot specify an empty prompt.")
if error == ServerErrors.INVALID_SIZE:
logger.warning(f'User "{kwargs["username"]}" sent an invalid size. Aborting!')
return("Invalid size. The image dimentions have to be multiples of 64.")
if error == ServerErrors.TOO_MANY_STEPS:
logger.warning(f'User "{kwargs["username"]}" sent too many steps ({kwargs["steps"]}). Aborting!')
return("Too many sampling steps. To allow resources for everyone, we allow only up to 100 steps.")
if error == ServerErrors.NO_PROXY:
logger.warning(f'Attempt to access outside reverse proxy')
return(f'Access allowed only through https')
if error == ServerErrors.NOT_ADMIN:
logger.warning(f'Non-admin user "{kwargs["username"]}" tried to use admin endpoint: "{kwargs["endpoint"]}". Aborting!')
return("You're not an admin. Sod off!")
if error == ServerErrors.MAINTENANCE_MODE:
logger.info(f'Rejecting endpoint "{kwargs["endpoint"]}" because server in maintenance mode.')
return("Server has enterred maintenance mode. Please try again later.")
if error == ServerErrors.NOT_OWNER:
logger.warning(f'User "{kwargs["username"]}" tried to modify server they do not own: "{kwargs["server_name"]}". Aborting!')
return("You're not the owner of this server!")
@REST_API.before_request
def limit_remote_addr():
logger.debug(request.remote_addr)
# if not allow_direct_connections and request.remote_addr != '127.0.0.1':
# error_msg = get_error(ServerErrors.NO_PROXY)
# abort(403, error_msg)
import os, logging
from horde import logger, args, HORDE
@REST_API.after_request
def after_request(response):
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS, PUT, DELETE"
response.headers["Access-Control-Allow-Headers"] = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization"
return response
class SyncGenerate(Resource):
def post(self, api_version = None):
parser = reqparse.RequestParser()
parser.add_argument("prompt", type=str, required=True, help="The prompt to generate from")
parser.add_argument("api_key", type=str, required=True, help="The API Key corresponding to a registered user")
parser.add_argument("params", type=dict, required=False, default={}, help="Extra generate params to send to the SD server")
parser.add_argument("servers", type=str, action='append', required=False, default=[], help="If specified, only the server with this ID will be able to generate this prompt")
args = parser.parse_args()
username = 'Anonymous'
user = None
if maintenance_mode:
return(f"{get_error(ServerErrors.MAINTENANCE_MODE, endpoint = 'SyncGenerate')}",503)
if args.api_key:
user = _db.find_user_by_api_key(args['api_key'])
if not user:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'prompt generation')}",401)
username = user.get_unique_alias()
if args['prompt'] == '':
return(f"{get_error(ServerErrors.EMPTY_PROMPT, username = username)}",400)
wp_count = _waiting_prompts.count_waiting_requests(user)
if wp_count >= user.concurrency:
return(f"{get_error(ServerErrors.TOO_MANY_PROMPTS, username = username, wp_count = wp_count)}",503)
if args["params"].get("length",512)%64:
return(f"{get_error(ServerErrors.INVALID_SIZE, username = username)}",400)
if args["params"].get("width",512)%64:
return(f"{get_error(ServerErrors.INVALID_SIZE, username = username)}",400)
if args["params"].get("steps",50) > 100:
return(f"{get_error(ServerErrors.TOO_MANY_STEPS, username = username, steps = args['params']['steps'])}",400)
wp = WaitingPrompt(
_db,
_waiting_prompts,
_processing_generations,
args["prompt"],
user,
args["params"],
servers=args["servers"],
)
server_found = False
for server in _db.servers.values():
if len(args.servers) and server.id not in args.servers:
continue
if server.can_generate(wp)[0]:
server_found = True
break
if not server_found:
del wp # Normally garbage collection will handle it, but doesn't hurt to be thorough
return("No active server found to fulfill this request. Please Try again later...", 503)
# if a server is available to fulfil this prompt, we activate it and add it to the queue to be generated
wp.activate()
while True:
time.sleep(1)
if wp.is_stale():
return("Prompt Request Expired", 500)
if wp.is_completed():
break
ret_dict = wp.get_status()['generations']
# We delete it from memory immediately to ensure we don't run out
wp.delete()
return(ret_dict, 200)
class AsyncGeneratePrompt(Resource):
decorators = [limiter.limit("3/minute")]
@logger.catch
def get(self, api_version = None, id = ''):
wp = _waiting_prompts.get_item(id)
if not wp:
return("ID not found", 404)
wp_status = wp.get_status()
# If the status is retrieved after the wp is done we clear it to free the ram
if wp_status["done"]:
wp.delete()
return(wp_status, 200)
class AsyncCheck(Resource):
# Increasing this until I can figure out how to pass original IP from reverse proxy
decorators = [limiter.limit("10/second")]
@logger.catch
def get(self, api_version = None, id = ''):
wp = _waiting_prompts.get_item(id)
if not wp:
return("ID not found", 404)
return(wp.get_lite_status(), 200)
class AsyncGenerate(Resource):
def post(self, api_version = None):
parser = reqparse.RequestParser()
parser.add_argument("prompt", type=str, required=True, help="The prompt to generate from")
parser.add_argument("api_key", type=str, required=True, help="The API Key corresponding to a registered user")
parser.add_argument("params", type=dict, required=False, default={}, help="Extra generate params to send to the SD server")
parser.add_argument("servers", type=str, action='append', required=False, default=[], help="If specified, only the server with this ID will be able to generate this prompt")
args = parser.parse_args()
username = 'Anonymous'
user = None
if maintenance_mode:
return(f"{get_error(ServerErrors.MAINTENANCE_MODE, endpoint = 'AsyncGenerate')}",503)
if args.api_key:
user = _db.find_user_by_api_key(args['api_key'])
if not user:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'prompt generation')}",401)
username = user.get_unique_alias()
if args['prompt'] == '':
return(f"{get_error(ServerErrors.EMPTY_PROMPT, username = username)}",400)
wp_count = _waiting_prompts.count_waiting_requests(user)
if wp_count >= user.concurrency:
return(f"{get_error(ServerErrors.TOO_MANY_PROMPTS, username = username, wp_count = wp_count)}",503)
if args["params"].get("length",512)%64:
return(f"{get_error(ServerErrors.INVALID_SIZE, username = username)}",400)
if args["params"].get("width",512)%64:
return(f"{get_error(ServerErrors.INVALID_SIZE, username = username)}",400)
if args["params"].get("steps",50) > 100:
return(f"{get_error(ServerErrors.TOO_MANY_STEPS, username = username, steps = args['params']['steps'])}",400)
wp = WaitingPrompt(
_db,
_waiting_prompts,
_processing_generations,
args["prompt"],
user,
args["params"],
servers=args["servers"],
)
server_found = False
for server in _db.servers.values():
if len(args.servers) and server.id not in args.servers:
continue
if server.can_generate(wp)[0]:
server_found = True
break
if not server_found:
del wp # Normally garbage collection will handle it, but doesn't hurt to be thorough
return("No active server found to fulfill this request. Please Try again later...", 503)
# if a server is available to fulfil this prompt, we activate it and add it to the queue to be generated
wp.activate()
return({"id":wp.id}, 200)
class PromptPop(Resource):
decorators = [limiter.limit("45/second")]
def post(self, api_version = None):
parser = reqparse.RequestParser()
parser.add_argument("api_key", type=str, required=True, help="The API Key corresponding to a registered user")
parser.add_argument("name", type=str, required=True, help="The server's unique name, to track contributions")
parser.add_argument("max_pixels", type=int, required=False, default=512, help="The maximum amount of pixels this server can generate")
parser.add_argument("priority_usernames", type=str, action='append', required=False, default=[], help="The usernames which get priority use on this server")
args = parser.parse_args()
skipped = {}
user = _db.find_user_by_api_key(args['api_key'])
if not user:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'server promptpop: ' + args['name'])}",401)
server = _db.find_server_by_name(args['name'])
if not server:
server = KAIServer(_db)
server.create(user, args['name'])
if user != server.user:
return(f"{get_error(ServerErrors.WRONG_CREDENTIALS,kai_instance = args['name'], username = user.get_unique_alias())}",401)
server.check_in(args['max_pixels'])
if server.maintenance:
return(f"Server has been put into maintenance mode by the owner",403)
if server.paused:
return({"id": None, "skipped": {}},200)
# This ensures that the priority requested by the bridge is respected
prioritized_wp = []
priority_users = [user]
## Start prioritize by bridge request ##
for priority_username in args.priority_usernames:
priority_user = _db.find_user_by_username(priority_username)
if priority_user:
priority_users.append(priority_user)
for priority_user in priority_users:
for wp in _waiting_prompts.get_all():
if wp.user == priority_user and wp.needs_gen():
prioritized_wp.append(wp)
## End prioritize by bridge request ##
for wp in _waiting_prompts.get_waiting_wp_by_kudos():
if wp not in prioritized_wp:
prioritized_wp.append(wp)
for wp in prioritized_wp:
check_gen = server.can_generate(wp)
if not check_gen[0]:
skipped_reason = check_gen[1]
skipped[skipped_reason] = skipped.get(skipped_reason,0) + 1
continue
ret = wp.start_generation(server)
return(ret, 200)
return({"id": None, "skipped": skipped}, 200)
class SubmitGeneration(Resource):
def post(self, api_version = None):
parser = reqparse.RequestParser()
parser.add_argument("id", type=str, required=True, help="The processing generation uuid")
parser.add_argument("api_key", type=str, required=True, help="The server's owner API key")
parser.add_argument("generation", type=str, required=False, default=[], help="The download location of the image")
parser.add_argument("seed", type=str, required=True, default=[], help="The seed of the generated image")
args = parser.parse_args()
procgen = _processing_generations.get_item(args['id'])
if not procgen:
return(f"{get_error(ServerErrors.INVALID_PROCGEN,id = args['id'])}",404)
user = _db.find_user_by_api_key(args['api_key'])
if not user:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'server submit: ' + args['name'])}",401)
if user != procgen.server.user:
return(f"{get_error(ServerErrors.WRONG_CREDENTIALS,kai_instance = args['name'], username = user.get_unique_alias())}",401)
kudos = procgen.set_generation(args['generation'], args['seed'])
if kudos == 0:
return(f"{get_error(ServerErrors.DUPLICATE_GEN,id = args['id'])}",400)
return({"reward": kudos}, 200)
class TransferKudos(Resource):
def post(self, api_version = None):
parser = reqparse.RequestParser()
parser.add_argument("username", type=str, required=True, help="The user ID which will receive the kudos")
parser.add_argument("api_key", type=str, required=True, help="The sending user's API key")
parser.add_argument("amount", type=int, required=False, default=100, help="The amount of kudos to transfer")
args = parser.parse_args()
user = _db.find_user_by_api_key(args['api_key'])
if not user:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'kudos transfer to: ' + args['username'])}",401)
ret = _db.transfer_kudos_from_apikey_to_username(args['api_key'],args['username'],args['amount'])
kudos = ret[0]
error = ret[1]
if error != 'OK':
return(f"{error}",400)
return({"transfered": kudos}, 200)
class AdminMaintenanceMode(Resource):
decorators = [limiter.limit("30/minute")]
def put(self, api_version = None):
global maintenance_mode
parser = reqparse.RequestParser()
parser.add_argument("api_key", type=str, required=True, help="The Admin API key")
parser.add_argument("active", type=bool, required=True, help="Star or stop maintenance mode")
args = parser.parse_args()
admin = _db.find_user_by_api_key(args['api_key'])
if not admin:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'Admin action: ' + 'AdminMaintenanceMode')}",401)
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
return(f"{get_error(ServerErrors.NOT_ADMIN, username = admin.get_unique_alias(), endpoint = 'AdminMaintenanceMode')}",401)
maintenance_mode = args['active']
return({"maintenance_mode": maintenance_mode}, 200)
class Servers(Resource):
@logger.catch
def get(self, api_version = None):
servers_ret = []
for server in _db.servers.values():
if server.is_stale():
continue
sdict = {
"name": server.name,
"id": server.id,
"max_pixels": server.max_pixels,
"megapixelsteps_generated": server.contributions,
"requests_fulfilled": server.fulfilments,
"kudos_rewards": server.kudos,
"kudos_details": server.kudos_details,
"performance": server.get_performance(),
"uptime": server.uptime,
"maintenance_mode": server.maintenance,
}
servers_ret.append(sdict)
return(servers_ret,200)
class ServerSingle(Resource):
@logger.catch
def get(self, api_version = None, server_id = ''):
server = _db.find_server_by_id(server_id)
if server:
sdict = {
"name": server.name,
"id": server.id,
"max_pixels": server.max_pixels,
"megapixelsteps_generated": server.contributions,
"requests_fulfilled": server.fulfilments,
"latest_performance": server.get_performance(),
"maintenance_mode": server.maintenance,
}
return(sdict,200)
else:
return("Not found", 404)
decorators = [limiter.limit("30/minute")]
def put(self, api_version = None, server_id = ''):
server = _db.find_server_by_id(server_id)
if not server:
return("Invalid Server ID", 404)
parser = reqparse.RequestParser()
parser.add_argument("api_key", type=str, required=True, help="The Admin or server owner API key")
parser.add_argument("maintenance", type=bool, required=False, help="Set to true to put this server into maintenance.")
parser.add_argument("paused", type=bool, required=False, help="Set to true to pause this server.")
args = parser.parse_args()
admin = _db.find_user_by_api_key(args['api_key'])
if not admin:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'User action: ' + 'PUT ServerSingle')}",401)
ret_dict = {}
# Both admins and owners can set the server to maintenance
if args.maintenance != None:
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
if admin != server.user:
return(f"{get_error(ServerErrors.NOT_OWNER, username = admin.get_unique_alias(), server_name = server.name)}",401)
server.maintenance = args.maintenance
ret_dict["maintenance"] = server.maintenance
# Only admins can set a server as paused
if args.paused != None:
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
return(f"{get_error(ServerErrors.NOT_ADMIN, username = admin.get_unique_alias(), endpoint = 'AdminModifyServer')}",401)
server.paused = args.paused
ret_dict["paused"] = server.paused
if not len(ret_dict):
return("No server modification selected!", 400)
return(ret_dict, 200)
# post shows also hidden server info
decorators = [limiter.limit("30/minute")]
def post(self, api_version = None, server_id = ''):
server = _db.find_server_by_id(server_id)
if not server:
return("Invalid Server ID", 404)
parser = reqparse.RequestParser()
parser.add_argument("api_key", type=str, required=True, help="The Admin or server owner API key")
args = parser.parse_args()
admin = _db.find_user_by_api_key(args['api_key'])
if not admin:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'User action: ' + 'PUT ServerSingle')}",401)
sdict = {
"name": server.name,
"id": server.id,
"max_pixels": server.max_pixels,
"megapixelsteps_generated": server.contributions,
"requests_fulfilled": server.fulfilments,
"latest_performance": server.get_performance(),
"maintenance": server.maintenance,
"paused": server.paused,
"owner": server.user.get_unique_alias(),
}
return(sdict,200)
class Users(Resource):
@logger.catch
def get(self, api_version = None):
user_dict = {}
for user in _db.users.values():
user_dict[user.get_unique_alias()] = {
"id": user.id,
"kudos": user.kudos,
"kudos_details": user.kudos_details,
"usage": user.usage,
"contributions": user.contributions,
"concurrency": user.concurrency,
}
return(user_dict,200)
class UserSingle(Resource):
@logger.catch
def get(self, api_version = None, user_id = ''):
logger.debug(user_id)
user = _db.find_user_by_id(user_id)
if user:
udict = {
"username": user.get_unique_alias(),
"kudos": user.kudos,
"usage": user.usage,
"contributions": user.contributions,
"concurrency": user.concurrency,
}
return(udict,200)
else:
return("Not found", 404)
decorators = [limiter.limit("30/minute")]
def put(self, api_version = None, user_id = ''):
user = user = _db.find_user_by_id(user_id)
if not user:
return(f"Invalid user_id: {user_id}",400)
parser = reqparse.RequestParser()
parser.add_argument("api_key", type=str, required=True, help="The Admin API key")
parser.add_argument("kudos", type=int, required=False, help="The amount of kudos to modify (can be negative)")
parser.add_argument("concurrency", type=int, required=False, help="The amount of concurrent request this user can have")
parser.add_argument("usage_multiplier", type=float, required=False, help="The amount by which to multiply the users kudos consumption")
args = parser.parse_args()
admin = _db.find_user_by_api_key(args['api_key'])
if not admin:
return(f"{get_error(ServerErrors.INVALID_API_KEY, subject = 'Admin action: ' + 'PUT UserSingle')}",401)
if not os.getenv("ADMINS") or admin.get_unique_alias() not in os.getenv("ADMINS"):
return(f"{get_error(ServerErrors.NOT_ADMIN, username = admin.get_unique_alias(), endpoint = 'AdminModifyUser')}",401)
ret_dict = {}
if args.kudos:
user.modify_kudos(args.kudos, 'admin')
ret_dict["new_kudos"] = user.kudos
if args.concurrency:
user.concurrency = args.concurrency
ret_dict["concurrency"] = user.concurrency
if args.usage_multiplier:
user.usage_multiplier = args.usage_multiplier
ret_dict["usage_multiplier"] = user.usage_multiplier
if not len(ret_dict):
return("No usermod operations selected!", 400)
return(ret_dict, 200)
class HordeLoad(Resource):
@logger.catch
def get(self, api_version = None):
load_dict = _waiting_prompts.count_totals()
load_dict["megapixelsteps_per_min"] = _db.stats.get_megapixelsteps_per_min()
load_dict["server_count"] = _db.count_active_servers()
load_dict["maintenance_mode"] = maintenance_mode
return(load_dict,200)
@logger.catch
@REST_API.route('/')
def index():
with open('index.md') as index_file:
index = index_file.read()
top_contributor = _db.get_top_contributor()
top_server = _db.get_top_server()
align_image = 0
big_image = align_image
while big_image == align_image:
big_image = random.randint(1, 5)
if not top_contributor or not top_server:
top_contributors = f'\n<img src="https://github.com/db0/Stable-Horde/blob/master/img/{big_image}.png?raw=true" width="800" />'
else:
top_contributors = f"""\n## Top Contributors
These are the people and servers who have contributed most to this horde.
### Users
This is the person whose server(s) have generated the most pixels for the horde.
#### {top_contributor.get_unique_alias()}
* {round(top_contributor.contributions['megapixelsteps'] / 1000,2)} Gigapixelsteps generated.
* {top_contributor.contributions['fulfillments']} requests fulfilled.
### Servers
This is the server which has generated the most pixels for the horde.
#### {top_server.name}
* {round(top_server.contributions/1000,2)} Gigapixelsteps generated.
* {top_server.fulfilments} request fulfillments.
* {top_server.get_human_readable_uptime()} uptime.
"""
policies = """
## Policies
[Privacy Policy](/privacy)
[Terms of Service](/terms)"""
totals = _db.get_total_usage()
findex = index.format(
stable_image = align_image,
avg_performance= round(_db.stats.get_request_avg() / 1000000,2),
total_pixels = round(totals["megapixelsteps"] / 1000,2),
total_fulfillments = totals["fulfilments"],
active_servers = _db.count_active_servers(),
total_queue = _waiting_prompts.count_total_waiting_generations(),
maintenance_mode = maintenance_mode,
)
head = """<head>
<title>Stable Horde</title>
<meta name="google-site-verification" content="pmLKyCEPKM5csKT9mW1ZbGLu2TX_wD0S5FCxWlmg_iI" />
</head>
"""
return(head + markdown(findex + top_contributors + policies))
@logger.catch
def get_oauth_id():
google_data = None
discord_data = None
github_data = None
authorized = False
if google.authorized:
google_user_info_endpoint = '/oauth2/v2/userinfo'
try:
google_data = google.get(google_user_info_endpoint).json()
authorized = True
except oauthlib.oauth2.rfc6749.errors.TokenExpiredError:
pass
if not authorized and discord.authorized:
discord_info_endpoint = '/api/users/@me'
try:
discord_data = discord.get(discord_info_endpoint).json()
authorized = True
except oauthlib.oauth2.rfc6749.errors.TokenExpiredError:
pass
if not authorized and github.authorized:
github_info_endpoint = '/user'
try:
github_data = github.get(github_info_endpoint).json()
authorized = True
except oauthlib.oauth2.rfc6749.errors.TokenExpiredError:
pass
oauth_id = None
if google_data:
oauth_id = f'g_{google_data["id"]}'
elif discord_data:
oauth_id = f'd_{discord_data["id"]}'
elif github_data:
oauth_id = f'gh_{github_data["id"]}'
return(oauth_id)
@logger.catch
@REST_API.route('/register', methods=['GET', 'POST'])
def register():
api_key = None
user = None
welcome = 'Welcome'
username = ''
pseudonymous = False
oauth_id = get_oauth_id()
if oauth_id:
user = _db.find_user_by_oauth_id(oauth_id)
if user:
username = user.username
if request.method == 'POST':
api_key = secrets.token_urlsafe(16)
if user:
username = request.form['username']
user.username = request.form['username']
user.api_key = api_key
else:
# Triggered when the user created a username without logging in
if not oauth_id:
oauth_id = str(uuid4())
pseudonymous = True
user = User(_db)
user.create(request.form['username'], oauth_id, api_key, None)
username = request.form['username']
if user:
welcome = f"Welcome back {user.get_unique_alias()}"
return render_template('register.html',
page_title="Join the Stable Horde!",
welcome=welcome,
user=user,
api_key=api_key,
username=username,
pseudonymous=pseudonymous,
oauth_id=oauth_id)
@logger.catch
@REST_API.route('/transfer', methods=['GET', 'POST'])
def transfer():
src_api_key = None
src_user = None
dest_username = None
kudos = None
error = None
welcome = 'Welcome'
oauth_id = get_oauth_id()
if oauth_id:
src_user = _db.find_user_by_oauth_id(oauth_id)
if not src_user:
# This probably means the user was deleted
oauth_id = None
if request.method == 'POST':
dest_username = request.form['username']
amount = request.form['amount']
if not amount.isnumeric():
kudos = 0
error = "Please enter a number in the kudos field"
# Triggered when the user submited without logging in
elif src_user:
ret = _db.transfer_kudos_to_username(src_user,dest_username,int(amount))
kudos = ret[0]
error = ret[1]
else:
ret = _db.transfer_kudos_from_apikey_to_username(request.form['src_api_key'],dest_username,int(amount))
kudos = ret[0]
error = ret[1]
if src_user:
welcome = f"Welcome back {src_user.get_unique_alias()}. You have {src_user.kudos} kudos remaining"
return render_template('transfer_kudos.html',
page_title="Kudos Transfer",
welcome=welcome,
kudos=kudos,
error=error,
dest_username=dest_username,
oauth_id=oauth_id)
@REST_API.route('/google/<return_to>')
def google_login(return_to):
global dance_return_to
dance_return_to = '/' + return_to
return redirect(url_for('google.login'))
@REST_API.route('/discord/<return_to>')
def discord_login(return_to):
global dance_return_to
dance_return_to = '/' + return_to
return redirect(url_for('discord.login'))
@REST_API.route('/github/<return_to>')
def github_login(return_to):
global dance_return_to
dance_return_to = '/' + return_to
return redirect(url_for('github.login'))
@REST_API.route('/finish_dance')
def finish_dance():
global dance_return_to
redirect_url = dance_return_to
dance_return_to = '/'
return redirect(redirect_url)
@REST_API.route('/privacy')
def privacy():
return render_template('privacy_policy.html')
@REST_API.route('/terms')
def terms():
return render_template('terms_of_service.html')
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('-i', '--insecure', action="store_true", help="If set, will use http instead of https (useful for testing)")
arg_parser.add_argument('-v', '--verbosity', action='count', default=0, help="The default logging level is ERROR or higher. This value increases the amount of logging seen in your screen")
arg_parser.add_argument('-q', '--quiet', action='count', default=0, help="The default logging level is ERROR or higher. This value decreases the amount of logging seen in your screen")
arg_parser.add_argument('-c', '--convert_flag', action='store', default=None, required=False, type=str, help="A special flag to convert from previous DB entries to newer and exit")
arg_parser.add_argument('-p', '--port', action='store', default=7001, required=False, type=int, help="Provide a different port to start with")
arg_parser.add_argument('--allow_direct_connections', action="store_true", required=False, default=False, help="If set, will allow connections outside the reverse proxy (useful for testing)")
if __name__ == "__main__":
global _db
global _waiting_prompts
global _processing_generations
args = arg_parser.parse_args()
allow_direct_connections = args.allow_direct_connections
set_logger_verbosity(args.verbosity)
quiesce_logger(args.quiet)
# Only setting this for the WSGI logs
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s',level=logging.WARNING)
_db = Database(convert_flag=args.convert_flag)
_waiting_prompts = PromptsIndex()
_processing_generations = GenerationsIndex()
google_client_id = os.getenv("GOOGLE_CLIENT_ID")
google_client_secret = os.getenv("GLOOGLE_CLIENT_SECRET")
discord_client_id = os.getenv("DISCORD_CLIENT_ID")
discord_client_secret = os.getenv("DISCORD_CLIENT_SECRET")
github_client_id = os.getenv("GITHUB_CLIENT_ID")
github_client_secret = os.getenv("GITHUB_CLIENT_SECRET")
REST_API.secret_key = os.getenv("secret_key")
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
from waitress import serve
logger.init("WSGI Server", status="Starting")
url_scheme = 'https'
if args.insecure:
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' # Disable this on prod
url_scheme = 'http'
google_blueprint = make_google_blueprint(
client_id = google_client_id,
client_secret = google_client_secret,
reprompt_consent = True,
redirect_url='/register',
scope = ["email"],
)
REST_API.register_blueprint(google_blueprint,url_prefix="/google")
discord_blueprint = make_discord_blueprint(
client_id = discord_client_id,
client_secret = discord_client_secret,
scope = ["identify"],
redirect_url='/finish_dance',
)
REST_API.register_blueprint(discord_blueprint,url_prefix="/discord")
github_blueprint = make_github_blueprint(
client_id = github_client_id,
client_secret = github_client_secret,
scope = ["identify"],
redirect_url='/finish_dance',
)
REST_API.register_blueprint(github_blueprint,url_prefix="/github")
api.add_resource(SyncGenerate, "/generate/sync","/api/<string:api_version>/generate/sync")
# Async is disabled due to the memory requirements of keeping images in running memory
api.add_resource(AsyncGenerate, "/generate/async","/api/<string:api_version>/generate/async")
api.add_resource(AsyncGeneratePrompt, "/generate/prompt/<string:id>","/api/<string:api_version>/generate/prompt/<string:id>")
api.add_resource(AsyncCheck, "/generate/prompt/<string:id>","/api/<string:api_version>/generate/check/<string:id>")
api.add_resource(PromptPop, "/generate/pop","/api/<string:api_version>/generate/pop")
api.add_resource(SubmitGeneration, "/generate/submit","/api/<string:api_version>/generate/submit")
api.add_resource(Users, "/users","/api/<string:api_version>/users")
api.add_resource(UserSingle, "/users/<string:user_id>","/api/<string:api_version>/users/<string:user_id>")
api.add_resource(Servers, "/servers","/api/<string:api_version>/servers")
api.add_resource(ServerSingle, "/servers/<string:server_id>","/api/<string:api_version>/servers/<string:server_id>")
api.add_resource(TransferKudos, "/api/<string:api_version>/kudos/transfer")
api.add_resource(HordeLoad, "/api/<string:api_version>/status/performance")
api.add_resource(AdminMaintenanceMode, "/api/<string:api_version>/admin/maintenance")
from waitress import serve
logger.init("WSGI Server", status="Starting")
serve(REST_API, host="0.0.0.0", port=args.port, url_scheme=url_scheme, threads=100, connection_limit=4096)
# REST_API.run(debug=True,host="0.0.0.0",port="5001")
serve(HORDE, host="127.0.0.1", port=args.port, url_scheme=url_scheme, threads=300, connection_limit=4096)
# HORDE.run(debug=True,host="0.0.0.0",port="5001")
logger.init("WSGI Server", status="Stopped")