This commit is contained in:
Patrick Jentsch 2020-03-29 19:40:29 +02:00
parent f7692102a5
commit d07b73781f
5 changed files with 188 additions and 141 deletions

View File

@ -1,5 +1,6 @@
from time import sleep from time import sleep
from .. import errors, specification from .. import specification
from ..errors import cl_error_lookup, error_lookup, cqp_error_lookup
import socket import socket
import struct import struct
@ -434,58 +435,18 @@ class APIClient:
byte_data = self.__recv_WORD() byte_data = self.__recv_WORD()
response_type = byte_data >> 8 response_type = byte_data >> 8
if response_type == specification.CL_ERROR: if response_type == specification.CL_ERROR:
raise self.__create_cl_error(byte_data) raise cl_error_lookup[byte_data]()
elif response_type == specification.CQP_ERROR: elif response_type == specification.CQP_ERROR:
raise self.__create_cqp_error(byte_data) raise cqp_error_lookup[byte_data]()
elif response_type == specification.DATA: elif response_type == specification.DATA:
return self.__recv_DATA(byte_data) return self.__recv_DATA(byte_data)
elif response_type == specification.ERROR: elif response_type == specification.ERROR:
raise self.__create_error(byte_data) raise error_lookup[byte_data]()
elif response_type == specification.STATUS: elif response_type == specification.STATUS:
return {'code': byte_data, 'msg': specification.lookup[byte_data]} return {'code': byte_data, 'msg': specification.lookup[byte_data]}
else: else:
raise Exception('Unknown response type: {}'.format(response_type)) raise Exception('Unknown response type: {}'.format(response_type))
def __create_cl_error(self, error_type):
if error_type == specification.CL_ERROR_NO_SUCH_ATTRIBUTE:
return errors.CLErrorNoSuchAttribute()
elif error_type == specification.CL_ERROR_WRONG_ATTRIBUTE_TYPE:
return errors.CLErrorWrongAttributeType()
elif error_type == specification.CL_ERROR_OUT_OF_RANGE:
return errors.CLErrorOutOfRange()
elif error_type == specification.CL_ERROR_REGEX:
return errors.CLErrorRegex()
elif error_type == specification.CL_ERROR_CORPUS_ACCESS:
return errors.CLErrorCorpusAccess()
elif error_type == specification.CL_ERROR_OUT_OF_MEMORY:
return errors.CLErrorOutOfMemory()
elif error_type == specification.CL_ERROR_INTERNAL:
return errors.CLErrorInternal()
else:
return errors.CLError(error_type)
def __create_cqp_error(self, error_type):
if error_type == specification.CQP_ERROR_GENERAL:
return errors.CQPErrorGeneral()
elif error_type == specification.CQP_ERROR_INVALID_FIELD:
return errors.CQPErrorInvalidField()
elif error_type == specification.CQP_ERROR_OUT_OF_RANGE:
return errors.CQPErrorOutOfRange()
else:
return errors.CQPError(error_type)
def __create_error(self, error_type):
if error_type == specification.ERROR_GENERAL_ERROR:
return errors.ErrorGeneralError()
elif error_type == specification.ERROR_CONNECT_REFUSED:
return errors.ErrorConnectRefused()
elif error_type == specification.ERROR_USER_ABORT:
return errors.ErrorUserAbort()
elif error_type == specification.ERROR_SYNTAX_ERROR:
return errors.ErrorSyntaxError()
else:
return errors.Error(error_type)
def __recv_DATA(self, data_type): def __recv_DATA(self, data_type):
if data_type == specification.DATA_BYTE: if data_type == specification.DATA_BYTE:
data = self.__recv_DATA_BYTE() data = self.__recv_DATA_BYTE()

View File

@ -1,3 +1,6 @@
from . import specification
class CQiException(Exception): class CQiException(Exception):
""" """
A base class from which all other exceptions inherit. A base class from which all other exceptions inherit.
@ -5,100 +8,173 @@ class CQiException(Exception):
catch this base exception. catch this base exception.
""" """
def __init__(self, *args, **kwargs):
super(CQiException, self).__init__(*args, **kwargs)
self.code = None
self.name = None
self.description = None
class Error(CQiException): class Error(CQiException):
# ERROR = 0x02 def __init__(self, *args, **kwargs):
pass super(Error, self).__init__(*args, **kwargs)
self.code = specification.ERROR
class ErrorGeneralError(Error): class ErrorGeneralError(Error):
# ERROR_GENERAL_ERROR = 0x0201 def __init__(self, *args, **kwargs):
pass super(ErrorGeneralError, self).__init__(*args, **kwargs)
self.code = specification.ERROR_GENERAL_ERROR
self.name = specification.lookup[self.code]
class ErrorConnectRefused(Error): class ErrorConnectRefused(Error):
# ERROR_CONNECT_REFUSED = 0x0202 def __init__(self, *args, **kwargs):
pass super(ErrorConnectRefused, self).__init__(*args, **kwargs)
self.code = specification.ERROR_CONNECT_REFUSED
self.name = specification.lookup[self.code]
class ErrorUserAbort(Error): class ErrorUserAbort(Error):
# ERROR_USER_ABORT = 0x0203 def __init__(self, *args, **kwargs):
pass super(ErrorUserAbort, self).__init__(*args, **kwargs)
self.code = specification.ERROR_USER_ABORT
self.name = specification.lookup[self.code]
class ErrorSyntaxError(Error): class ErrorSyntaxError(Error):
# ERROR_SYNTAX_ERROR = 0x0204 def __init__(self, *args, **kwargs):
pass super(ErrorSyntaxError, self).__init__(*args, **kwargs)
self.code = specification.ERROR_SYNTAX_ERROR
self.name = specification.lookup[self.code]
class CLError(CQiException): class CLError(CQiException):
# CL_ERROR = 0x04 def __init__(self, *args, **kwargs):
pass super(CLError, self).__init__(*args, **kwargs)
self.code = specification.CL_ERROR
class CLErrorNoSuchAttribute(CLError): class CLErrorNoSuchAttribute(CLError):
# CL_ERROR_NO_SUCH_ATTRIBUTE = 0x0401 def __init__(self, *args, **kwargs):
# returned if CQi server couldn't open attribute super(CLErrorNoSuchAttribute, self).__init__(*args, **kwargs)
pass self.code = specification.CL_ERROR_NO_SUCH_ATTRIBUTE
self.name = specification.lookup[self.code]
self.description = "CQi server couldn't open attribute"
class CLErrorWrongAttributeType(CLError): class CLErrorWrongAttributeType(CLError):
# CL_ERROR_WRONG_ATTRIBUTE_TYPE = 0x0402
# CDA_EATTTYPE # CDA_EATTTYPE
pass def __init__(self, *args, **kwargs):
super(CLErrorWrongAttributeType, self).__init__(*args, **kwargs)
self.code = specification.CL_ERROR_WRONG_ATTRIBUTE_TYPE
self.name = specification.lookup[self.code]
class CLErrorOutOfRange(CLError): class CLErrorOutOfRange(CLError):
# CL_ERROR_OUT_OF_RANGE = 0x0403
# CDA_EIDORNG, CDA_EIDXORNG, CDA_EPOSORNG # CDA_EIDORNG, CDA_EIDXORNG, CDA_EPOSORNG
pass def __init__(self, *args, **kwargs):
super(CLErrorOutOfRange, self).__init__(*args, **kwargs)
self.code = specification.CL_ERROR_OUT_OF_RANGE
self.name = specification.lookup[self.code]
class CLErrorRegex(CLError): class CLErrorRegex(CLError):
# CL_ERROR_REGEX = 0x0404
# CDA_EPATTERN (not used), CDA_EBADREGEX # CDA_EPATTERN (not used), CDA_EBADREGEX
pass def __init__(self, *args, **kwargs):
super(CLErrorRegex, self).__init__(*args, **kwargs)
self.code = specification.CL_ERROR_REGEX
self.name = specification.lookup[self.code]
class CLErrorCorpusAccess(CLError): class CLErrorCorpusAccess(CLError):
# CL_ERROR_CORPUS_ACCESS = 0x0405
# CDA_ENODATA # CDA_ENODATA
pass def __init__(self, *args, **kwargs):
super(CLErrorCorpusAccess, self).__init__(*args, **kwargs)
self.code = specification.CL_ERROR_CORPUS_ACCESS
self.name = specification.lookup[self.code]
class CLErrorOutOfMemory(CLError): class CLErrorOutOfMemory(CLError):
# CL_ERROR_OUT_OF_MEMORY = 0x0406
# CDA_ENOMEM # CDA_ENOMEM
# this means the CQi server has run out of memory; def __init__(self, *args, **kwargs):
# try discarding some other corpora and/or subcorpora super(CLErrorOutOfMemory, self).__init__(*args, **kwargs)
pass self.code = specification.CL_ERROR_OUT_OF_MEMORY
self.name = specification.lookup[self.code]
self.description = ('CQi server has run out of memory; try discarding '
'some other corpora and/or subcorpora')
class CLErrorInternal(CLError): class CLErrorInternal(CLError):
# CL_ERROR_INTERNAL = 0x0407
# CDA_EOTHER, CDA_ENYI # CDA_EOTHER, CDA_ENYI
# this is the classical 'please contact technical support' error def __init__(self, *args, **kwargs):
pass super(CLErrorInternal, self).__init__(*args, **kwargs)
self.code = specification.CL_ERROR_INTERNAL
self.name = specification.lookup[self.code]
self.description = "Classical 'please contact technical support' error"
class CQPError(CQiException): class CQPError(CQiException):
# CQP_ERROR = 0x05
# CQP error messages yet to be defined # CQP error messages yet to be defined
pass def __init__(self, *args, **kwargs):
super(CQPError, self).__init__(*args, **kwargs)
self.code = specification.CQP_ERROR
class CQPErrorGeneral(CQPError): class CQPErrorGeneral(CQPError):
# CQP_ERROR_GENERAL = 0x0501 def __init__(self, *args, **kwargs):
pass super(CQPErrorGeneral, self).__init__(*args, **kwargs)
# CQP_ERROR_NO_SUCH_CORPUS = 0x0502 self.code = specification.CQP_ERROR_GENERAL
self.name = specification.lookup[self.code]
class CQPErrorNoSuchCorpus(CQPError):
def __init__(self, *args, **kwargs):
super(CQPErrorNoSuchCorpus, self).__init__(*args, **kwargs)
self.code = specification.CQP_ERROR_NO_SUCH_CORPUS
self.name = specification.lookup[self.code]
class CQPErrorInvalidField(CQPError): class CQPErrorInvalidField(CQPError):
# CQP_ERROR_INVALID_FIELD = 0x0503 def __init__(self, *args, **kwargs):
pass super(CQPErrorInvalidField, self).__init__(*args, **kwargs)
self.code = specification.CQP_ERROR_INVALID_FIELD
self.name = specification.lookup[self.code]
class CQPErrorOutOfRange(CQPError): class CQPErrorOutOfRange(CQPError):
# CQP_ERROR_OUT_OF_RANGE = 0x0504 def __init__(self, *args, **kwargs):
# various cases where a number is out of range super(CQPErrorOutOfRange, self).__init__(*args, **kwargs)
pass self.code = specification.CQP_ERROR_OUT_OF_RANGE
self.name = specification.lookup[self.code]
self.description = 'A number is out of range'
error_lookup = {
specification.ERROR: Error,
specification.ERROR_GENERAL_ERROR: ErrorGeneralError,
specification.ERROR_CONNECT_REFUSED: ErrorConnectRefused,
specification.ERROR_USER_ABORT: ErrorUserAbort,
specification.ERROR_SYNTAX_ERROR: ErrorSyntaxError
}
cl_error_lookup = {
specification.CL_ERROR: CLError,
specification.CL_ERROR_NO_SUCH_ATTRIBUTE: CLErrorNoSuchAttribute,
specification.CL_ERROR_WRONG_ATTRIBUTE_TYPE: CLErrorWrongAttributeType,
specification.CL_ERROR_OUT_OF_RANGE: CLErrorOutOfRange,
specification.CL_ERROR_REGEX: CLErrorRegex,
specification.CL_ERROR_CORPUS_ACCESS: CLErrorCorpusAccess,
specification.CL_ERROR_OUT_OF_MEMORY: CLErrorOutOfMemory,
specification.CL_ERROR_INTERNAL: CLErrorInternal
}
cqp_error_lookup = {
specification.CQP_ERROR: CQPError,
specification.CQP_ERROR_GENERAL: CQPErrorGeneral,
specification.CQP_ERROR_NO_SUCH_CORPUS: CQPErrorNoSuchCorpus,
specification.CQP_ERROR_INVALID_FIELD: CQPErrorInvalidField,
specification.CQP_ERROR_OUT_OF_RANGE: CQPErrorOutOfRange
}

View File

@ -33,37 +33,46 @@ def pj_init_corpus_analysis(corpus_id):
def pj_corpus_analysis_query(query): def pj_corpus_analysis_query(query):
client = pj_corpus_analysis_clients.get(request.sid) client = pj_corpus_analysis_clients.get(request.sid)
if client is None: if client is None:
response = {'code': 404, 'msg': 'Failed Dependency'} response = {'code': 404, 'desc': 'No client found for this session',
'msg': 'Failed Dependency'}
socketio.emit('pj_corpus_analysis_query', response, room=request.sid) socketio.emit('pj_corpus_analysis_query', response, room=request.sid)
return return
try:
corpus = client.corpora.get('CORPUS') corpus = client.corpora.get('CORPUS')
except cqi.errors.CQiException as e:
response = {'code': 500, 'desc': None, 'msg': 'Internal Server Error',
'payload': {'code': e.code, 'desc': e.description,
'msg': e.name}}
socketio.emit('pj_corpus_analysis_query', response, room=request.sid)
return
try: try:
results = corpus.query(query) results = corpus.query(query)
except cqi.errors.CQiException as e: except cqi.errors.CQiException as e:
response = {'code': 1, 'msg': str(e)} response = {'code': 500, 'desc': None, 'msg': 'Internal Server Error',
'payload': {'code': e.code, 'desc': e.description,
'msg': e.name}}
socketio.emit('pj_corpus_analysis_query', response, room=request.sid) socketio.emit('pj_corpus_analysis_query', response, room=request.sid)
else: return
response = {'code': 200, 'msg': 'OK', response = {'code': 200, 'desc': None, 'msg': 'OK',
'data': {'num_matches': results.size}} 'payload': {'num_matches': results.size}}
socketio.emit('pj_corpus_analysis_query', response, room=request.sid) socketio.emit('pj_corpus_analysis_query', response, room=request.sid)
chunk_size = 100 chunk_size = 100
chunk_start = 0 chunk_start = 0
context = 100 context = 100
progress = 0 progress = 0
while chunk_start <= results.size: while chunk_start <= results.size:
chunk = results.export(context=context, offset=chunk_start, chunk = results.export(context=context, cutoff=chunk_size,
cutoff=chunk_size) offset=chunk_start)
if (results.size == 0): if (results.size == 0):
progress = 100 progress = 100
else: else:
progress = ((chunk_start + chunk_size) / results.size) * 100 progress = ((chunk_start + chunk_size) / results.size) * 100
progress = min(100, int(math.ceil(progress))) progress = min(100, int(math.ceil(progress)))
socketio.emit('pj_corpus_analysis_query_results', response = {'code': 200, 'desc': None, 'msg': 'OK',
{'chunk': chunk, 'payload': {'chunk': chunk, 'progress': progress}}
'progress': progress}, socketio.emit('pj_corpus_analysis_query_results', response,
room=request.sid) room=request.sid)
chunk_start += chunk_size chunk_start += chunk_size
chunk_size = 250
def pj_corpus_analysis_session_handler(app, corpus_id, user_id, session_id): def pj_corpus_analysis_session_handler(app, corpus_id, user_id, session_id):
@ -72,11 +81,11 @@ def pj_corpus_analysis_session_handler(app, corpus_id, user_id, session_id):
corpus = Corpus.query.get(corpus_id) corpus = Corpus.query.get(corpus_id)
user = User.query.get(user_id) user = User.query.get(user_id)
if corpus is None: if corpus is None:
response = {'code': 404, 'msg': 'Not Found'} response = {'code': 404, 'desc': None, 'msg': 'Not Found'}
socketio.emit('pj_corpus_analysis_init', response, room=session_id) socketio.emit('pj_corpus_analysis_init', response, room=session_id)
return return
elif not (corpus.creator == user or user.is_administrator()): elif not (corpus.creator == user or user.is_administrator()):
response = {'code': 403, 'msg': 'Forbidden'} response = {'code': 403, 'desc': None, 'msg': 'Forbidden'}
socketio.emit('pj_corpus_analysis_init', response, room=session_id) socketio.emit('pj_corpus_analysis_init', response, room=session_id)
return return
while corpus.status != 'analysing': while corpus.status != 'analysing':
@ -85,8 +94,11 @@ def pj_corpus_analysis_session_handler(app, corpus_id, user_id, session_id):
client = cqi.CQiClient('corpus_{}_analysis'.format(corpus_id)) client = cqi.CQiClient('corpus_{}_analysis'.format(corpus_id))
try: try:
client.connect() client.connect()
except cqi.errors.CQiException: except cqi.errors.CQiException as e:
response = {'code': 500, 'msg': 'Internal Server Error'} response = {'code': 500, 'desc': None,
'msg': 'Internal Server Error',
'payload': {'code': e.code, 'desc': e.description,
'msg': e.name}}
socketio.emit('pj_corpus_analysis_init', response, room=session_id) socketio.emit('pj_corpus_analysis_init', response, room=session_id)
return return
pj_corpus_analysis_clients[session_id] = client pj_corpus_analysis_clients[session_id] = client
@ -94,7 +106,7 @@ def pj_corpus_analysis_session_handler(app, corpus_id, user_id, session_id):
pj_corpus_analysis_sessions[corpus_id] = [session_id] pj_corpus_analysis_sessions[corpus_id] = [session_id]
else: else:
pj_corpus_analysis_sessions[corpus_id].append(session_id) pj_corpus_analysis_sessions[corpus_id].append(session_id)
response = {'code': 200, 'msg': 'OK'} response = {'code': 200, 'desc': None, 'msg': 'OK'}
socketio.emit('pj_corpus_analysis_init', response, room=session_id) socketio.emit('pj_corpus_analysis_init', response, room=session_id)
''' Observe analysis session ''' ''' Observe analysis session '''
while session_id in connected_sessions: while session_id in connected_sessions:

View File

@ -8,48 +8,46 @@ class CorpusAnalysisClient {
socket.on("pj_corpus_analysis_init", (response) => { socket.on("pj_corpus_analysis_init", (response) => {
if (response.code === 200) { if (response.code === 200) {
console.log(`pj_corpus_analysis_init: ${response.code} - ${response.msg}`); console.log(`pj_corpus_analysis_init: ${response.code} - ${response.msg}`);
if (this.callbacks.init != undefined) {this.callbacks.init(response.msg);} if (this.callbacks.init != undefined) {this.callbacks.init(response.payload);}
if (this.displays.init != undefined) {this.displays.init.setVisibilityByStatus("success");} if (this.displays.init != undefined) {this.displays.init.setVisibilityByStatus("success");}
} else { } else {
if (this.displays.init != undefined) { errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
if (this.displays.init.errorContainer != undefined) {this.displays.init.errorContainer.innerHTML = `<p class="red-text"><i class="material-icons tiny">error</i> Error ${response.code}: ${response.msg}</p>`;} if (this.displays.init.errorContainer != undefined) {this.displays.init.errorContainer.innerHTML = `<p class="red-text"><i class="material-icons tiny">error</i> ${errorText}</p>`;}
this.displays.init.setVisibilityByStatus("error"); if (this.displays.init != undefined) {this.displays.init.setVisibilityByStatus("error");}
} console.error(`pj_corpus_analysis_init: ${errorText}`);
console.error(`pj_corpus_analysis_init: ${response.code} - ${response.msg}`);
} }
}); });
socket.on("pj_corpus_analysis_query", (response) => { socket.on("pj_corpus_analysis_query", (response) => {
var errorText;
if (response.code === 200) { if (response.code === 200) {
console.log(`pj_corpus_analysis_query: ${response.code} - ${response.msg}`); console.log(`pj_corpus_analysis_query: ${response.code} - ${response.msg}`);
if (this.callbacks.query != undefined) {this.callbacks.query(response.data);} if (this.callbacks.query != undefined) {this.callbacks.query(response.payload);}
if (this.displays.query != undefined) {this.displays.query.setVisibilityByStatus("success");} if (this.displays.query != undefined) {this.displays.query.setVisibilityByStatus("success");}
} else { } else {
nopaque.flash("error", `Error ${response.code}: ${response.msg}`); errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
if (this.displays.query.errorContainer != undefined) {this.displays.query.errorContainer.innerHTML = `<p class="red-text"><i class="material-icons tiny">error</i> Error ${response.code}: ${response.msg}</p>`;} nopaque.flash("error", errorText);
if (this.displays.query.errorContainer != undefined) {this.displays.query.errorContainer.innerHTML = `<p class="red-text"><i class="material-icons tiny">error</i> ${errorText}</p>`;}
if (this.displays.query != undefined) {this.displays.query.setVisibilityByStatus("error");} if (this.displays.query != undefined) {this.displays.query.setVisibilityByStatus("error");}
console.error(`pj_corpus_analysis_query: ${response.code} - ${response.msg}`) console.error(`pj_corpus_analysis_query: ${errorText}`);
} }
}); });
socket.on("pj_corpus_analysis_query_results", (response) => { socket.on("pj_corpus_analysis_query_results", (response) => {
if (this.callbacks.query_results != undefined) {this.callbacks.query_results(response);} if (this.callbacks.query_results != undefined) {this.callbacks.query_results(response.payload);}
}); });
} }
init() { init() {
if (this.displays.init) {
if (this.displays.init.errorContainer != undefined) {this.displays.init.errorContainer.innerHTML == "";} if (this.displays.init.errorContainer != undefined) {this.displays.init.errorContainer.innerHTML == "";}
this.displays.init.setVisibilityByStatus("waiting"); if (this.displays.init != undefined) {this.displays.init.setVisibilityByStatus("waiting");}
}
this.socket.emit("pj_corpus_analysis_init", this.corpusId); this.socket.emit("pj_corpus_analysis_init", this.corpusId);
} }
query(query) { query(query) {
if (this.displays.query) {
if (this.displays.query.errorContainer != undefined) {this.displays.query.errorContainer.innerHTML == "";} if (this.displays.query.errorContainer != undefined) {this.displays.query.errorContainer.innerHTML == "";}
this.displays.query.setVisibilityByStatus("waiting"); if (this.displays.query != undefined) {this.displays.query.setVisibilityByStatus("waiting");}
}
nopaque.socket.emit("pj_corpus_analysis_query", query); nopaque.socket.emit("pj_corpus_analysis_query", query);
} }

View File

@ -131,24 +131,24 @@
client.setDisplay("init", initDisplay); client.setDisplay("init", initDisplay);
client.setCallback("init", () => {initModal.close();}); client.setCallback("init", () => {initModal.close();});
client.setDisplay("query", queryDisplay); client.setDisplay("query", queryDisplay);
client.setCallback("query", (response) => { client.setCallback("query", (payload) => {
// This is called when a query was successfull // This is called when a query was successfull
results = {matches: [], cpos_lookup: {}, text_lookup: {}}; results = {matches: [], cpos_lookup: {}, text_lookup: {}};
queryResultsDeterminateElement.style.width = "0%"; queryResultsDeterminateElement.style.width = "0%";
receivedMatchNumElement.innerText = "0"; receivedMatchNumElement.innerText = "0";
textLookupNumElement.innerText = "0"; textLookupNumElement.innerText = "0";
matchNumElement.innerText = response.num_matches; matchNumElement.innerText = payload.num_matches;
}); });
client.setCallback("query_results", (response) => { client.setCallback("query_results", (payload) => {
// This is called when results are transmitted. // This is called when results are transmitted.
if (response.progress === 100) { if (payload.progress === 100) {
queryResultsProgressElement.classList.add("hide"); queryResultsProgressElement.classList.add("hide");
} }
queryResultsDeterminateElement.style.width = `${response.progress}%`; queryResultsDeterminateElement.style.width = `${payload.progress}%`;
results.matches.push(...response.chunk.matches); results.matches.push(...payload.chunk.matches);
receivedMatchNumElement.innerText = `${results.matches.length}`; receivedMatchNumElement.innerText = `${results.matches.length}`;
Object.assign(results.cpos_lookup, response.chunk.cpos_lookup); Object.assign(results.cpos_lookup, payload.chunk.cpos_lookup);
Object.assign(results.text_lookup, response.chunk.text_lookup); Object.assign(results.text_lookup, payload.chunk.text_lookup);
textLookupNumElement.innerText = `${Object.keys(results.text_lookup).length}`; textLookupNumElement.innerText = `${Object.keys(results.text_lookup).length}`;
}); });