commit 7476163494cd9b6cb8b99ebda4b261b861ff88b2
Author: KoboldAI <83558899+KoboldAI@users.noreply.github.com>
Date: Sun May 2 18:46:45 2021 -0400
Initial Upload
diff --git a/aiserver.py b/aiserver.py
new file mode 100644
index 00000000..4e84daeb
--- /dev/null
+++ b/aiserver.py
@@ -0,0 +1,564 @@
+#==================================================================#
+# KoboldAI Client
+# Version: Dev-0.1
+# By: KoboldAIDev
+#==================================================================#
+
+from os import path, getcwd
+import json
+import easygui
+
+#==================================================================#
+# Variables & Storage
+#==================================================================#
+# Terminal tags for colored text
+class colors:
+ HEADER = '\033[95m'
+ OKBLUE = '\033[94m'
+ OKCYAN = '\033[96m'
+ OKGREEN = '\033[92m'
+ WARNING = '\033[93m'
+ FAIL = '\033[91m'
+ ENDC = '\033[0m'
+ BOLD = '\033[1m'
+ UNDERLINE = '\033[4m'
+
+# Transformers models
+modellist = [
+ ["InferKit API (requires API key)", "InferKit"],
+ ["GPT Neo 1.3B", "EleutherAI/gpt-neo-1.3B"],
+ ["GPT Neo 2.7B", "EleutherAI/gpt-neo-2.7B"],
+ ["GPT-2", "gpt2"],
+ ["GPT-2 Med", "gpt2-medium"],
+ ["GPT-2 Large", "gpt2-large"],
+ ["GPT-2 XL", "gpt2-xl"]
+ ]
+
+# Variables
+class vars:
+ lastact = "" # The last action submitted to the generator
+ model = ''
+ noai = False # Runs the script without starting up the transformers pipeline
+ aibusy = False # Stops submissions while the AI is working
+ max_length = 500 # Maximum number of tokens to submit per action
+ genamt = 60 # Amount of text for each action to generate
+ rep_pen = 1.2 # Generator repetition_penalty
+ temp = 1.1 # Generator temperature
+ gamestarted = False
+ prompt = ""
+ memory = ""
+ actions = []
+ mode = "play" # Whether the interface is in play, memory, or edit mode
+ editln = 0 # Which line was last selected in Edit Mode
+ url = "https://api.inferkit.com/v1/models/standard/generate" # InferKit API URL
+ apikey = ""
+ savedir = getcwd()+"\stories\\newstory.json"
+
+#==================================================================#
+# Startup
+#==================================================================#
+# Select a model to run
+print("{0}Welcome to the KoboldAI Client!\nSelect an AI model to continue:{1}\n".format(colors.OKCYAN, colors.ENDC))
+i = 1
+for m in modellist:
+ print(" {0} - {1}".format(i, m[0]))
+ i += 1
+print(" ");
+modelsel = 0
+while(vars.model == ''):
+ modelsel = int(input("Model #> "))
+ if(modelsel > 0 and modelsel <= len(modellist)):
+ vars.model = modellist[modelsel-1][1]
+ else:
+ print("{0}Please enter a valid selection.{1}".format(colors.FAIL, colors.ENDC))
+
+# Ask for API key if InferKit was selected
+if(vars.model == "InferKit"):
+ if(not path.exists("client.settings")):
+ # If the client settings file doesn't exist, create it
+ print("{0}Please enter your InferKit API key:{1}\n".format(colors.OKCYAN, colors.ENDC))
+ vars.apikey = input("Key> ")
+ # Write API key to file
+ file = open("client.settings", "w")
+ file.write("{\"apikey\": \""+vars.apikey+"\"}")
+ file.close()
+ else:
+ # Otherwise open it up and get the key
+ file = open("client.settings", "r")
+ vars.apikey = json.load(file)["apikey"]
+ file.close()
+
+# Set logging level to reduce chatter from Flask
+import logging
+log = logging.getLogger('werkzeug')
+log.setLevel(logging.ERROR)
+
+# Start flask & SocketIO
+print("{0}Initializing Flask... {1}".format(colors.HEADER, colors.ENDC), end="")
+from flask import Flask, render_template
+from flask_socketio import SocketIO, emit
+app = Flask(__name__)
+app.config['SECRET KEY'] = 'secret!'
+socketio = SocketIO(app)
+print("{0}OK!{1}".format(colors.OKGREEN, colors.ENDC))
+
+# Start transformers and create pipeline
+if(vars.model != "InferKit"):
+ if(not vars.noai):
+ print("{0}Initializing transformers, please wait...{1}".format(colors.HEADER, colors.ENDC))
+ from transformers import pipeline, GPT2Tokenizer
+
+ generator = pipeline('text-generation', model=vars.model)
+ tokenizer = GPT2Tokenizer.from_pretrained(vars.model)
+ print("{0}OK! {1} pipeline created!{2}".format(colors.OKGREEN, vars.model, colors.ENDC))
+else:
+ # Import requests library for HTTPS calls
+ import requests
+
+ # Set generator variables to match InferKit's capabilities
+ vars.max_length = 3000
+ vars.genamt = 200
+
+# Set up Flask routes
+@app.route('/')
+@app.route('/index')
+def index():
+ return render_template('index.html')
+
+#============================ METHODS =============================#
+
+#==================================================================#
+# Event triggered when browser SocketIO is loaded and connects to server
+#==================================================================#
+@socketio.on('connect')
+def do_connect():
+ print("{0}Client connected!{1}".format(colors.OKGREEN, colors.ENDC))
+ emit('from_server', {'cmd': 'connected'})
+ if(not vars.gamestarted):
+ setStartState()
+ else:
+ # Game in session, send current game data and ready state to browser
+ refresh_story()
+ if(vars.mode == "play"):
+ if(not vars.aibusy):
+ emit('from_server', {'cmd': 'setgamestate', 'data': 'ready'})
+ else:
+ emit('from_server', {'cmd': 'setgamestate', 'data': 'wait'})
+ elif(vars.mode == "edit"):
+ emit('from_server', {'cmd': 'editmode', 'data': 'true'})
+ elif(vars.mode == "memory"):
+ emit('from_server', {'cmd': 'memmode', 'data': 'true'})
+
+#==================================================================#
+# Event triggered when browser SocketIO sends data to the server
+#==================================================================#
+@socketio.on('message')
+def get_message(msg):
+ print("{0}Data recieved:{1}{2}".format(colors.OKGREEN, msg, colors.ENDC))
+ # Submit action
+ if(msg['cmd'] == 'submit'):
+ if(vars.mode == "play"):
+ actionsubmit(msg['data'])
+ elif(vars.mode == "edit"):
+ editsubmit(msg['data'])
+ elif(vars.mode == "memory"):
+ memsubmit(msg['data'])
+ # Retry Action
+ elif(msg['cmd'] == 'retry'):
+ if(vars.aibusy):
+ return
+ set_aibusy(1)
+ # Remove last action if possible and resubmit
+ if(len(vars.actions) > 0):
+ vars.actions.pop()
+ refresh_story()
+ calcsubmit('')
+ # Back/Undo Action
+ elif(msg['cmd'] == 'back'):
+ if(vars.aibusy):
+ return
+ # Remove last index of actions and refresh game screen
+ if(len(vars.actions) > 0):
+ vars.actions.pop()
+ refresh_story()
+ # EditMode Action
+ elif(msg['cmd'] == 'edit'):
+ if(vars.mode == "play"):
+ vars.mode = "edit"
+ emit('from_server', {'cmd': 'editmode', 'data': 'true'})
+ elif(vars.mode == "edit"):
+ vars.mode = "play"
+ emit('from_server', {'cmd': 'editmode', 'data': 'false'})
+ # EditLine Action
+ elif(msg['cmd'] == 'editline'):
+ editrequest(int(msg['data']))
+ # DeleteLine Action
+ elif(msg['cmd'] == 'delete'):
+ deleterequest()
+ elif(msg['cmd'] == 'memory'):
+ togglememorymode()
+ elif(msg['cmd'] == 'save'):
+ saveRequest()
+ elif(msg['cmd'] == 'load'):
+ loadRequest()
+ elif(msg['cmd'] == 'newgame'):
+ newGameRequest()
+
+#==================================================================#
+#
+#==================================================================#
+def setStartState():
+ emit('from_server', {'cmd': 'updatescreen', 'data': 'Welcome to KoboldAI Client! You are running '+vars.model+'.
Please load a game or enter a prompt below to begin!'})
+ emit('from_server', {'cmd': 'setgamestate', 'data': 'start'})
+
+#==================================================================#
+#
+#==================================================================#
+def actionsubmit(data):
+ if(vars.aibusy):
+ return
+ set_aibusy(1)
+ if(not vars.gamestarted):
+ vars.gamestarted = True # Start the game
+ vars.prompt = data # Save this first action as the prompt
+ emit('from_server', {'cmd': 'updatescreen', 'data': 'Please wait, generating story...'}) # Clear the startup text from game screen
+ calcsubmit(data) # Run the first action through the generator
+ else:
+ # Dont append submission if it's a blank/continue action
+ if(data != ""):
+ vars.actions.append(data)
+ calcsubmit(data)
+
+#==================================================================#
+# Take submitted text and build the text to be given to generator
+#==================================================================#
+def calcsubmit(txt):
+ # For all transformers models
+ if(vars.model != "InferKit"):
+ vars.lastact = txt # Store most recent action in memory (is this still needed?)
+
+ # Calculate token budget
+ prompttkns = tokenizer.encode(vars.prompt)
+ lnprompt = len(prompttkns)
+
+ memtokens = tokenizer.encode(vars.memory)
+ lnmem = len(memtokens)
+
+ budget = vars.max_length - lnprompt - lnmem - vars.genamt
+
+ if(len(vars.actions) == 0):
+ # First/Prompt action
+ subtxt = vars.memory + vars.prompt
+ lnsub = len(memtokens+prompttkns)
+ generate(subtxt, lnsub+1, lnsub+vars.genamt)
+ else:
+ # Get most recent action tokens up to our budget
+ tokens = []
+ for n in range(len(vars.actions)):
+ if(budget <= 0):
+ break
+ acttkns = tokenizer.encode(vars.actions[(-1-n)])
+ tknlen = len(acttkns)
+ if(tknlen < budget):
+ tokens = acttkns + tokens
+ budget -= tknlen
+ else:
+ count = budget * -1
+ tokens = acttkns[count:] + tokens
+
+ # Add mmory & prompt tokens to beginning of bundle
+ tokens = memtokens + prompttkns + tokens
+
+ # Send completed bundle to generator
+ ln = len(tokens)
+ generate (
+ tokenizer.decode(tokens),
+ ln+1,
+ ln+vars.genamt
+ )
+ # For InferKit web API
+ else:
+ budget = vars.max_length - len(vars.prompt) - len(vars.memory) - 1
+ subtxt = ""
+ for n in range(len(vars.actions)):
+ if(budget <= 0):
+ break
+ actlen = len(vars.actions[(-1-n)])
+ if(actlen < budget):
+ subtxt = vars.actions[(-1-n)] + subtxt
+ budget -= actlen
+ else:
+ count = budget * -1
+ subtxt = vars.actions[(-1-n)][count:] + subtxt
+
+ # Add mmory & prompt tokens to beginning of bundle
+ if(vars.memory != ""):
+ subtxt = vars.memory + "\n" + vars.prompt + subtxt
+ else:
+ subtxt = vars.prompt + subtxt
+
+ # Send it!
+ ikrequest(subtxt)
+
+#==================================================================#
+# Send text to generator and deal with output
+#==================================================================#
+def generate(txt, min, max):
+ print("{0}Min:{1}, Max:{2}, Txt:{3}{4}".format(colors.WARNING, min, max, txt, colors.ENDC))
+ genout = generator(
+ txt,
+ do_sample=True,
+ min_length=min,
+ max_length=max,
+ repetition_penalty=vars.rep_pen,
+ temperature=vars.temp
+ )[0]["generated_text"]
+ print("{0}{1}{2}".format(colors.OKCYAN, genout, colors.ENDC))
+ vars.actions.append(getnewcontent(genout))
+ refresh_story()
+ emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)})
+
+ set_aibusy(0)
+
+#==================================================================#
+# Replaces returns and newlines with HTML breaks
+#==================================================================#
+def formatforhtml(txt):
+ return txt.replace("\\r", "
").replace("\\n", "
").replace('\n', '
').replace('\r', '
')
+
+#==================================================================#
+# Strips submitted text from the text returned by the AI
+#==================================================================#
+def getnewcontent(txt):
+ ln = len(vars.actions)
+ if(ln == 0):
+ delim = vars.prompt
+ else:
+ delim = vars.actions[-1]
+
+ return (txt.split(delim)[-1])
+
+#==================================================================#
+# Sends the current story content to the Game Screen
+#==================================================================#
+def refresh_story():
+ txt = ''+vars.prompt+''
+ i = 1
+ for item in vars.actions:
+ txt = txt + ''+item+''
+ i += 1
+ emit('from_server', {'cmd': 'updatescreen', 'data': formatforhtml(txt)})
+
+#==================================================================#
+# Sets the logical and display states for the AI Busy condition
+#==================================================================#
+def set_aibusy(state):
+ if(state):
+ vars.aibusy = True
+ emit('from_server', {'cmd': 'setgamestate', 'data': 'wait'})
+ else:
+ vars.aibusy = False
+ emit('from_server', {'cmd': 'setgamestate', 'data': 'ready'})
+
+#==================================================================#
+#
+#==================================================================#
+def editrequest(n):
+ if(n == 0):
+ txt = vars.prompt
+ else:
+ txt = vars.actions[n-1]
+
+ vars.editln = n
+ emit('from_server', {'cmd': 'setinputtext', 'data': txt})
+ emit('from_server', {'cmd': 'enablesubmit', 'data': ''})
+
+#==================================================================#
+#
+#==================================================================#
+def editsubmit(data):
+ if(vars.editln == 0):
+ vars.prompt = data
+ else:
+ vars.actions[vars.editln-1] = data
+
+ vars.mode = "play"
+ refresh_story()
+ emit('from_server', {'cmd': 'texteffect', 'data': vars.editln})
+ emit('from_server', {'cmd': 'editmode', 'data': 'false'})
+
+#==================================================================#
+#
+#==================================================================#
+def deleterequest():
+ # Don't delete prompt
+ if(vars.editln == 0):
+ # Send error message
+ pass
+ else:
+ del vars.actions[vars.editln-1]
+ vars.mode = "play"
+ refresh_story()
+ emit('from_server', {'cmd': 'editmode', 'data': 'false'})
+
+#==================================================================#
+# Toggles the game mode for memory editing and sends UI commands
+#==================================================================#
+def togglememorymode():
+ if(vars.mode == "play"):
+ vars.mode = "memory"
+ emit('from_server', {'cmd': 'memmode', 'data': 'true'})
+ emit('from_server', {'cmd': 'setinputtext', 'data': vars.memory})
+ elif(vars.mode == "memory"):
+ vars.mode = "play"
+ emit('from_server', {'cmd': 'memmode', 'data': 'false'})
+
+#==================================================================#
+# Commit changes to Memory storage
+#==================================================================#
+def memsubmit(data):
+ # Maybe check for length at some point
+ # For now just send it to storage
+ vars.memory = data
+ vars.mode = "play"
+ emit('from_server', {'cmd': 'memmode', 'data': 'false'})
+
+#==================================================================#
+# Assembles game data into a request to InferKit API
+#==================================================================#
+def ikrequest(txt):
+ # Log request to console
+ print("{0}Len:{1}, Txt:{2}{3}".format(colors.WARNING, len(txt), txt, colors.ENDC))
+
+ # Build request JSON data
+ reqdata = {
+ 'forceNoEnd': True,
+ 'length': vars.genamt,
+ 'prompt': {
+ 'isContinuation': False,
+ 'text': txt
+ },
+ 'startFromBeginning': False,
+ 'streamResponse': False,
+ 'temperature': vars.temp,
+ 'topP': 0.9
+ }
+
+ # Create request
+ req = requests.post(
+ vars.url,
+ json = reqdata,
+ headers = {
+ 'Authorization': 'Bearer '+vars.apikey
+ }
+ )
+
+ # Deal with the response
+ if(req.status_code == 200):
+ genout = req.json()["data"]["text"]
+ print("{0}{1}{2}".format(colors.OKCYAN, genout, colors.ENDC))
+ vars.actions.append(genout)
+ refresh_story()
+ emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)})
+
+ set_aibusy(0)
+ else:
+ # Send error message to web client
+ er = req.json()
+ if("error" in er):
+ code = er["error"]["extensions"]["code"]
+ elif("errors" in er):
+ code = er["errors"][0]["extensions"]["code"]
+
+ errmsg = "InferKit API Error: {0} - {1}".format(req.status_code, code)
+ emit('from_server', {'cmd': 'errmsg', 'data': errmsg})
+ set_aibusy(0)
+
+#==================================================================#
+# Forces UI to Play mode
+#==================================================================#
+def exitModes():
+ if(vars.mode == "edit"):
+ emit('from_server', {'cmd': 'editmode', 'data': 'false'})
+ elif(vars.mode == "memory"):
+ emit('from_server', {'cmd': 'memmode', 'data': 'false'})
+ vars.mode = "play"
+
+#==================================================================#
+# Save the story to a file
+#==================================================================#
+def saveRequest():
+ path = easygui.filesavebox(default=vars.savedir)
+
+ if(path != None):
+ # Leave Edit/Memory mode before continuing
+ exitModes()
+ # Save path for future saves
+ vars.savedir = path
+ # Build json to write
+ js = {}
+ #js["maxlegth"] = vars.max_length # This causes problems when switching to/from InfraKit
+ #js["genamt"] = vars.genamt
+ js["rep_pen"] = vars.rep_pen
+ js["temp"] = vars.temp
+ js["gamestarted"] = vars.gamestarted
+ js["prompt"] = vars.prompt
+ js["memory"] = vars.memory
+ js["actions"] = vars.actions
+ js["savedir"] = path
+ # Write it
+ file = open(path, "w")
+ file.write(json.dumps(js))
+ file.close()
+
+#==================================================================#
+# Load a stored story from a file
+#==================================================================#
+def loadRequest():
+ path = easygui.fileopenbox(default=vars.savedir) # Returns None on cancel
+
+ if(path != None):
+ # Leave Edit/Memory mode before continuing
+ exitModes()
+ # Read file contents into JSON object
+ file = open(path, "r")
+ js = json.load(file)
+ # Copy file contents to vars
+ #vars.max_length = js["maxlegth"] # This causes problems when switching to/from InfraKit
+ #vars.genamt = js["genamt"]
+ vars.rep_pen = js["rep_pen"]
+ vars.temp = js["temp"]
+ vars.gamestarted = js["gamestarted"]
+ vars.prompt = js["prompt"]
+ vars.memory = js["memory"]
+ vars.actions = js["actions"]
+ vars.savedir = js["savedir"]
+ file.close()
+ # Refresh game screen
+ refresh_story()
+ emit('from_server', {'cmd': 'setgamestate', 'data': 'ready'})
+
+#==================================================================#
+# Starts a new story
+#==================================================================#
+def newGameRequest():
+ # Ask for confirmation
+ if(easygui.ccbox("Really start new Story?","Please Confirm")):
+ # Leave Edit/Memory mode before continuing
+ exitModes()
+ # Clear vars values
+ vars.gamestarted = False
+ vars.prompt = ""
+ vars.memory = ""
+ vars.actions = []
+ vars.savedir = getcwd()+"\stories\\newstory.json"
+ # Refresh game screen
+ setStartState()
+
+
+#==================================================================#
+# Start Flask/SocketIO (Blocking, so this must be last method!)
+#==================================================================#
+if __name__ == "__main__":
+ print("{0}Server started!\rYou may now connect with a browser at http://127.0.0.1:5000/{1}".format(colors.OKGREEN, colors.ENDC))
+ socketio.run(app)
diff --git a/install_requirements.bat b/install_requirements.bat
new file mode 100644
index 00000000..39a9971b
--- /dev/null
+++ b/install_requirements.bat
@@ -0,0 +1 @@
+pip install -r requirements.txt
\ No newline at end of file
diff --git a/play.bat b/play.bat
new file mode 100644
index 00000000..7c422175
--- /dev/null
+++ b/play.bat
@@ -0,0 +1 @@
+aiserver.py
\ No newline at end of file
diff --git a/readme.txt b/readme.txt
new file mode 100644
index 00000000..b4116d7f
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,43 @@
+Thanks for checking out the KoboldAI Client!
+
+[ABOUT]
+
+This is a test release of a quickly-assembled front-end for multiple local & remote AI models.
+The purpose is to provide a smoother, web-based UI experience than the various command-line AI apps.
+I'm pushing this out now that the major quality-of-life fearures have been roughed in (generate,
+undo, edit-by-line, memory, save/load, etc), which means there will probably be bugs.
+
+This application uses Transformers (https://huggingface.co/transformers/) to interact with the AI models
+via Tensorflow. Tensorflow has CUDA/GPU support for shorter generation times, but I do not have anything
+in this test release to set up CUDA/GPU support on your system. If you have a high-end GPU with
+sufficient VRAM to run your model of choice, see (https://www.tensorflow.org/install/gpu) for
+instructions on enabling GPU support.
+
+Transformers/Tensorflow can still be used on CPU if you do not have high-end hardware, but generation
+times will be much longer. Alternatively, KoboldAI also supports InferKit (https://inferkit.com/).
+This will allow you to send requests to a remotely hosted Megatron-11b model for fast generation times
+on any hardware. This is a paid service, but signing up for a free account will let you generate up
+to 40,000 characters, and the free account will work with KoboldAI.
+
+[SETUP]
+
+1. Install Python. (https://www.python.org/downloads/)
+ (Development was done on 3.7, I have not tested newer versions)
+2. When installing Python make sure "pip" is selected under Optional features.
+ (If pip isn't working, run the installer again and choose Modify to choose Optional fearures.)
+3. Run install_requirements.bat.
+ (This will install the necessary python packages via pip)
+4. Run play.bat
+5. Select a model from the list. Flask will start and give you a message that it's ready to connect.
+6. Open a web browser and enter http://127.0.0.1:5000/
+
+[FOR INFERKIT INTEGRATION]
+
+If you would like to use InferKit's Megatron-11b model, sign up for a free account on their website.
+https://inferkit.com/
+After verifying your email address, sign in and click on your profile picture in the top right.
+In the drop down menu, click "API Key".
+On the API Key page, click "Reveal API Key" and copy it. When starting KoboldAI and selecting the
+InferKit API model, you will be asked to paste your API key into the terminal. After entering,
+the API key will be stored in the client.settings file for future use.
+You can see your remaining budget for generated characters on their website under "Billing & Usage".
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..775de178
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,6 @@
+transformers == 4.5.1
+tensorflow-gpu == 2.4.1
+Flask == 1.1.2
+Flask-SocketIO == 5.0.1
+requests == 2.25.1
+easygui == 0.98.2
\ No newline at end of file