diff --git a/aiserver.py b/aiserver.py index a5d7e339..a1d734cd 100644 --- a/aiserver.py +++ b/aiserver.py @@ -10,6 +10,7 @@ import re import tkinter as tk from tkinter import messagebox import json +import collections from typing import Literal, Union import requests @@ -23,6 +24,7 @@ import fileops import gensettings from utils import debounce import utils +import structures import breakmodel #==================================================================# @@ -78,7 +80,7 @@ class vars: memory = "" # Text submitted to memory field authornote = "" # Text submitted to Author's Note field andepth = 3 # How far back in history to append author's note - actions = [] # Array of actions submitted by user and AI + actions = structures.KoboldStoryRegister() # Actions submitted by user and AI worldinfo = [] # Array of World Info key/value objects badwords = [] # Array of str/chr values that should be removed from output badwordsids = [] # Tokenized array of badwords @@ -883,8 +885,9 @@ def actionretry(data): set_aibusy(1) # Remove last action if possible and resubmit if(len(vars.actions) > 0): + last_key = vars.actions.get_last_key() vars.actions.pop() - remove_story_chunk(len(vars.actions) + 1) + remove_story_chunk(last_key) calcsubmit('') #==================================================================# @@ -895,9 +898,9 @@ def actionback(): return # Remove last index of actions and refresh game screen if(len(vars.actions) > 0): - action_index = len(vars.actions) + last_key = vars.actions.get_last_key() vars.actions.pop() - remove_story_chunk(len(vars.actions) + 1) + remove_story_chunk(last_key) #==================================================================# # Take submitted text and build the text to be given to generator @@ -964,11 +967,13 @@ def calcsubmit(txt): forceanote = True # Get most recent action tokens up to our budget - for n in range(actionlen): + n = 0 + for key in reversed(vars.actions): + chunk = vars.actions[key] if(budget <= 0): break - acttkns = tokenizer.encode(vars.actions[(-1-n)]) + acttkns = tokenizer.encode(chunk) tknlen = len(acttkns) if(tknlen < budget): tokens = acttkns + tokens @@ -984,6 +989,7 @@ def calcsubmit(txt): if(anotetxt != ""): tokens = anotetkns + tokens # A.N. len already taken from bdgt anoteadded = True + n += 1 # If we're not using the prompt every time and there's still budget left, # add some prompt. @@ -1038,17 +1044,19 @@ def calcsubmit(txt): subtxt = "" prompt = vars.prompt - for n in range(actionlen): + n = 0 + for key in reversed(vars.actions): + chunk = vars.actions[key] if(budget <= 0): break - actlen = len(vars.actions[(-1-n)]) + actlen = len(chunk) if(actlen < budget): - subtxt = vars.actions[(-1-n)] + subtxt + subtxt = chunk + subtxt budget -= actlen else: count = budget * -1 - subtxt = vars.actions[(-1-n)][count:] + subtxt + subtxt = chunk[count:] + subtxt budget = 0 break @@ -1065,6 +1073,7 @@ def calcsubmit(txt): if(anotetxt != ""): subtxt = anotetxt + subtxt # A.N. len already taken from bdgt anoteadded = True + n += 1 # Did we get to add the A.N.? If not, do it here if(anotetxt != ""): @@ -1157,7 +1166,7 @@ def genresult(genout): # Add formatted text to Actions array and refresh the game screen vars.actions.append(genout) update_story_chunk('last') - emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)}, broadcast=True) + emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key()}, broadcast=True) #==================================================================# # Send generator sequences to the UI for selection @@ -1184,7 +1193,7 @@ def selectsequence(n): return vars.actions.append(vars.genseqs[int(n)]["generated_text"]) update_story_chunk('last') - emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)}, broadcast=True) + emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key()}, broadcast=True) emit('from_server', {'cmd': 'hidegenseqs', 'data': ''}, broadcast=True) vars.genseqs = [] @@ -1243,7 +1252,7 @@ def sendtocolab(txt, min, max): # Add formatted text to Actions array and refresh the game screen #vars.actions.append(genout) #refresh_story() - #emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)}) + #emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key()}) set_aibusy(0) else: @@ -1315,7 +1324,9 @@ def applyoutputformatting(txt): #==================================================================# def refresh_story(): text_parts = ['', html.escape(vars.prompt), ''] - for idx, item in enumerate(vars.actions, start=1): + for idx in vars.actions: + item = vars.actions[idx] + idx += 1 item = html.escape(item) if vars.adventure: # Add special formatting to adventure actions item = vars.acregex_ui.sub('\\1', item) @@ -1335,7 +1346,7 @@ def update_story_chunk(idx: Union[int, Literal['last']]): refresh_story() return - idx = len(vars.actions) + idx = vars.actions.get_last_key() + 1 if idx == 0: text = vars.prompt @@ -1349,7 +1360,7 @@ def update_story_chunk(idx: Union[int, Literal['last']]): item = vars.acregex_ui.sub('\\1', item) chunk_text = f'{formatforhtml(item)}' - emit('from_server', {'cmd': 'updatechunk', 'data': {'index': idx, 'html': chunk_text, 'last': (idx == len(vars.actions))}}, broadcast=True) + emit('from_server', {'cmd': 'updatechunk', 'data': {'index': idx, 'html': chunk_text, 'last': (idx == vars.actions.get_last_key())}}, broadcast=True) #==================================================================# @@ -1601,10 +1612,19 @@ def checkworldinfo(txt): txt = "" depth += 1 + if(ln > 0): + chunks = collections.deque() + i = 0 + for key in reversed(vars.actions): + chunk = vars.actions[key] + chunks.appendleft(chunk) + if(i == depth): + break + if(ln >= depth): - txt = "".join(vars.actions[(depth*-1):]) + txt = "".join(chunks) elif(ln > 0): - txt = vars.prompt + "".join(vars.actions[(depth*-1):]) + txt = vars.prompt + "".join(chunks) elif(ln == 0): txt = vars.prompt @@ -1697,7 +1717,7 @@ def ikrequest(txt): print("{0}{1}{2}".format(colors.CYAN, genout, colors.END)) vars.actions.append(genout) update_story_chunk('last') - emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)}, broadcast=True) + emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key()}, broadcast=True) set_aibusy(0) else: @@ -1747,7 +1767,7 @@ def oairequest(txt, min, max): print("{0}{1}{2}".format(colors.CYAN, genout, colors.END)) vars.actions.append(genout) update_story_chunk('last') - emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)}, broadcast=True) + emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key()}, broadcast=True) set_aibusy(0) else: @@ -1825,7 +1845,7 @@ def saveRequest(savpath): js["prompt"] = vars.prompt js["memory"] = vars.memory js["authorsnote"] = vars.authornote - js["actions"] = vars.actions + js["actions"] = tuple(vars.actions.values()) js["worldinfo"] = [] # Extract only the important bits of WI @@ -1876,10 +1896,14 @@ def loadRequest(loadpath): vars.gamestarted = js["gamestarted"] vars.prompt = js["prompt"] vars.memory = js["memory"] - vars.actions = js["actions"] vars.worldinfo = [] vars.lastact = "" vars.lastctx = "" + + del vars.actions + vars.actions = structures.KoboldStoryRegister() + for s in js["actions"]: + vars.actions.append(s) # Try not to break older save files if("authorsnote" in js): @@ -1988,7 +2012,7 @@ def importgame(): vars.prompt = "" vars.memory = ref["memory"] vars.authornote = ref["authorsNote"] if type(ref["authorsNote"]) is str else "" - vars.actions = [] + vars.actions = structures.KoboldStoryRegister() vars.worldinfo = [] vars.lastact = "" vars.lastctx = "" @@ -2047,7 +2071,7 @@ def importAidgRequest(id): vars.prompt = js["promptContent"] vars.memory = js["memory"] vars.authornote = js["authorsNote"] - vars.actions = [] + vars.actions = structures.KoboldStoryRegister() vars.worldinfo = [] vars.lastact = "" vars.lastctx = "" @@ -2113,7 +2137,7 @@ def newGameRequest(): vars.gamestarted = False vars.prompt = "" vars.memory = "" - vars.actions = [] + vars.actions = structures.KoboldStoryRegister() vars.authornote = "" vars.worldinfo = [] diff --git a/static/application.js b/static/application.js index c7eab749..d6574b8f 100644 --- a/static/application.js +++ b/static/application.js @@ -880,17 +880,6 @@ $(document).ready(function(){ let index = msg.data; // Remove the chunk game_text.children(`#n${index}`).remove() - // Shift all existing chunks by 1 - index++; - while(true) { - const chunk = game_text.children(`#n${index}`) - if(chunk.length === 0) { - break; - } - const newIndex = index - 1; - chunk.attr('n', newIndex.toString()).attr('id', `n${newIndex}`); - index++; - } hide([$('#curtain')]); } else if(msg.cmd == "setgamestate") { // Enable or Disable buttons diff --git a/structures.py b/structures.py new file mode 100644 index 00000000..287f92c1 --- /dev/null +++ b/structures.py @@ -0,0 +1,40 @@ +import collections +from typing import Iterable, Tuple + + +class KoboldStoryRegister(collections.OrderedDict): + ''' + Complexity-optimized class for keeping track of story chunks + ''' + + def __init__(self, sequence: Iterable[Tuple[int, str]] = ()): + super().__init__(sequence) + self.__next_id: int = len(sequence) + + def append(self, v: str) -> None: + self[self.__next_id] = v + self.increment_id() + + def pop(self) -> str: + return self.popitem()[1] + + def get_first_key(self) -> int: + return next(iter(self)) + + def get_last_key(self) -> int: + return next(reversed(self)) + + def __getitem__(self, k: int) -> str: + return super().__getitem__(k) + + def __setitem__(self, k: int, v: str) -> None: + return super().__setitem__(k, v) + + def increment_id(self) -> None: + self.__next_id += 1 + + def get_next_id(self) -> int: + return self.__next_id + + def set_next_id(self, x: int) -> None: + self.__next_id = x