moved webui and updated poetry packages
Code Quality Pipeline / Test (push) Successful in 4m5s
CI Pipeline / Test (pull_request) Failing after 3m42s
CI Pipeline / Build and Publish (pull_request) Has been skipped

This commit is contained in:
Brian Bjarke Jensen
2024-05-20 19:19:29 +02:00
parent 72c60170a7
commit fd9140093d
54 changed files with 511 additions and 460 deletions
+3 -2
View File
@@ -30,7 +30,8 @@ ENV PATH="${POETRY_HOME}/bin:$PATH"
# install runtime dependencies
WORKDIR ${APP_HOME}
COPY poetry.lock pyproject.toml ./
RUN --mount=type=cache,target=${POETRY_CACHE_DIR} poetry install --without model
RUN --mount=type=cache,target=${POETRY_CACHE_DIR} poetry install \
--with shared,web_ui
# final stage
FROM python:3.12-slim-bookworm
@@ -48,7 +49,7 @@ RUN mkdir -p /home/app && \
# add code while changing ownership
WORKDIR $APP_HOME
COPY --chown=app:app ./core ./core
COPY --chown=app:app ./shared ./shared
COPY --chown=app:app ./web_ui/src ./src
# change to the app user
+13
View File
@@ -0,0 +1,13 @@
services:
web:
image: visual_critical_discourse_analysis:test
container_name: visual_critical_discourse_analysis_alone
build:
context: ../
dockerfile: ./web_ui/Dockerfile
ports:
- 8050:8050
env_file:
- ../server.env
environment:
- ENV=TEST
+1 -1
View File
@@ -1,3 +1,3 @@
from __future__ import annotations
from .app import app
from .init_app import init_app
-251
View File
@@ -1,251 +0,0 @@
from __future__ import annotations
import logging
import os
import dash_bootstrap_components as dbc
from dash import ALL
from dash import Dash
from dash import Input
from dash import Output
from dash import State
from dash_auth import BasicAuth
from pydantic import ValidationError
from .layout import app_layout
from core.database import connect
from core.database import count_documents
from core.database import get_visual_communication
from core.database import NoDocumentFoundException
from core.database import upsert_annotation
from core.database import upsert_visual_communication
from core.database import VisualCommunication
from core.dto import ModelData
# setup app
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.title = 'visual critical discourse analysis'.title()
app.layout = app_layout
server = app.server
# setup authentication
AUTH_DICT = {
os.getenv('DASH_AUTH_USERNAME'): os.getenv('DASH_AUTH_PASSWORD'),
}
BasicAuth(app, AUTH_DICT)
# connect to database
collection, db, client = connect()
# define callbacks
@app.callback(
Output('alert-element', 'is_open'),
Output('alert-element', 'children'),
Input('alert-message', 'data'),
)
def show_alert(
msg: str | None,
):
if msg is None or msg == '':
return False, ''
logging.info(f'updated alert message: {msg}')
return True, msg
@app.callback(
Output('success-element', 'is_open'),
Output('success-element', 'children'),
Input('success-message', 'data'),
)
def show_success(
msg: str | None,
):
if msg is None or msg == '':
return False, ''
logging.info(f"updated success message: {msg}")
return True, msg
@app.callback(
Output('progress-bar', 'value'),
Output('progress-bar', 'max'),
Output('progress-bar', 'label'),
Input('next-button', 'n_clicks'),
Input('upload-data', 'filename'),
)
def update_progress_bar(
n_clicks: int,
filename_list: list[str] | None,
):
logging.info('began updating progress bar')
global collection
# get values from database
num_total = count_documents(
collection=collection,
only_with_annotation=False,
)
num_handled = count_documents(
collection=collection,
only_with_annotation=True,
)
limit = int(num_total/20)
label_str = f"{num_handled}/{num_total}" if num_handled >= limit else ''
return num_handled, num_total, label_str
@app.callback(
Output('success-message', 'data', allow_duplicate=True),
Output('alert-message', 'data', allow_duplicate=True),
Input('upload-data', 'contents'),
State('upload-data', 'filename'),
prevent_initial_call=True,
)
def upload_images(
content_list: list[str] | None,
filename_list: list[str] | None,
):
logging.info('began uploading images')
global collection
# stop early if possible
if content_list is None or filename_list is None:
logging.warning('content_list is None -> nothing to upload')
return None, 'nothing selected to upload'.title()
# build list of visual communication
visual_communication_list = []
for content, filename in zip(content_list, filename_list):
try:
image = VisualCommunication.decode_image(content)
vis_com = VisualCommunication(
name=filename,
image=image,
)
except Exception as exc:
logging.warning(
(
'failed creating VisualCommunication object '
f"from file {filename}"
),
exc,
)
else:
visual_communication_list.append(vis_com)
# upsert documents
success = upsert_visual_communication(
collection=collection,
visual_communication_list=visual_communication_list,
)
if success:
logging.info(f"succesfully uploaded {len(filename_list)} images")
return 'successfully uploaded images'.title(), None
logging.warning('failed inserting images into database')
return None, 'failed uploading images'.title()
@app.callback(
Output('alert-message', 'data', allow_duplicate=True),
Output('vis-com-name', 'data'),
Output('image-container', 'src'),
Output({'type': 'annotation', 'index': ALL}, 'value'),
Input('next-button', 'n_clicks'),
State('vis-com-name', 'data'),
State('image-container', 'src'),
State({'type': 'annotation', 'index': ALL}, 'id'),
State({'type': 'annotation', 'index': ALL}, 'value'),
prevent_initial_call=True,
)
def cycle_visual_communication_data(
n_clicks: int,
vis_com_name: str,
image_src: str,
annotation_keys: list,
annotation_values: list,
):
logging.info('began cycling visual communication data')
global collection
# prepare default response
response = [
'',
vis_com_name,
image_src,
annotation_values,
]
# check if next-button clicked
if n_clicks == 0:
logging.info('stopping early: next-button has not yet been clicked')
return response
# check if visual communication name is set
if len(vis_com_name) > 0:
logging.info('saving annotations to database: %s', vis_com_name)
try:
# extract option keys
annotation_keys = [
elem['index']
for elem in annotation_keys
]
# ensure all options are set
logging.info(annotation_keys)
for option, value in zip(annotation_keys, annotation_values):
if value is None:
raise ValueError(f"{option} is not set")
# prepare data to save
annotation_keys = [
elem.replace(' ', '_')
for elem
in annotation_keys
]
annotation_values = [
elem.replace(' ', '_').lower()
for elem
in annotation_values
]
annotation_map = {
key: value
for key, value
in zip(annotation_keys, annotation_values)
}
# instantiate ModelOutputs object
annotations = ModelData.from_annotations(**annotation_map)
# save data to database
upsert_annotation(
collection=collection,
vis_com_name=vis_com_name,
annotations=annotations,
)
except (ValueError, ValidationError) as exc:
msg = f"failed saving annotation: {exc}"
logging.warning(msg)
response[0] = msg
return tuple(response)
# get new visual communication
logging.info('trying to get new visual communication')
try:
# get data
vis_com = get_visual_communication(
collection=collection,
with_annotation=False,
)
# set variables
vis_com_name = vis_com.name
image_src = vis_com.webencoded_image()
if vis_com.prediction is not None:
# TODO: update to use optional predictions
pass
else:
# reset annotations
annotation_values = [None for elem in annotation_values]
except NoDocumentFoundException:
msg = 'no unannotated data in database'
logging.warning(msg)
response[0] = msg
return tuple(response)
else:
response[1] = vis_com_name
response[2] = image_src
response[3] = annotation_values
logging.info('finished getting visual communication: %s', vis_com_name)
return tuple(response)
if __name__ == '__main__':
app.run(debug=True)
+260
View File
@@ -0,0 +1,260 @@
"""Definition of init_app function."""
from __future__ import annotations
import logging
import os
import dash_bootstrap_components as dbc
from dash import ALL
from dash import Dash
from dash import Input
from dash import Output
from dash import State
from dash_auth import BasicAuth
from pydantic import ValidationError
from pymongo.collection import Collection
from .layout import app_layout
from shared.database import count_documents
from shared.database import get_visual_communication
from shared.database import NoDocumentFoundException
from shared.database import upsert_annotation
from shared.database import upsert_visual_communication
from shared.database import VisualCommunication
from shared.dto import ModelData
def init_app(
collection: Collection,
) -> Dash:
"""Initialise web UI application."""
# setup app
app = Dash(
name='visual_critical_discourse_analysis_web_ui',
external_stylesheets=[
dbc.themes.BOOTSTRAP,
],
)
app.title = 'visual critical discourse analysis'.title()
app.layout = app_layout
# setup authentication
auth_dict = {
os.getenv('DASH_AUTH_USERNAME'): os.getenv('DASH_AUTH_PASSWORD'),
}
BasicAuth(app, auth_dict)
# define callback: show warning
@app.callback(
Output('warning-element', 'is_open'),
Output('warning-element', 'children'),
Input('warning-message', 'data'),
)
def show_warning(
msg: str | None,
) -> tuple[bool, str]:
"""Show warning message in web ui."""
if msg is None or msg == '':
return False, ''
logging.debug('updated warning message: %s', msg)
return True, msg
# define callback: show info
@app.callback(
Output('info-element', 'is_open'),
Output('info-element', 'children'),
Input('info-message', 'data'),
)
def show_info(
msg: str | None,
) -> tuple[bool, str]:
"""Show info message in web ui."""
if msg is None or msg == '':
return False, ''
logging.debug('updated info message: %s', msg)
return True, msg
# define callback: update progress bar
@app.callback(
Output('progress-bar', 'value'),
Output('progress-bar', 'max'),
Output('progress-bar', 'label'),
Input('next-button', 'n_clicks'),
Input('upload-data', 'filename'),
)
def update_progress_bar(
n_clicks: int,
filename_list: list[str] | None,
) -> tuple[int, int, str]:
"""Update progress bar in web ui."""
assert isinstance(n_clicks, int)
if filename_list is not None:
assert isinstance(filename_list, list)
# get values from database
num_total = count_documents(
collection=collection,
only_with_annotation=False,
)
num_handled = count_documents(
collection=collection,
only_with_annotation=True,
)
limit = int(num_total/20)
label_str = f"{
num_handled
}/{num_total}" if num_handled >= limit else ''
return num_handled, num_total, label_str
# define callback: upload image
@app.callback(
Output('info-message', 'data', allow_duplicate=True),
Output('warning-message', 'data', allow_duplicate=True),
Input('upload-data', 'contents'),
State('upload-data', 'filename'),
prevent_initial_call=True,
)
def upload_images(
content_list: list[str] | None,
filename_list: list[str] | None,
) -> tuple[str | None, str | None]:
"""Upload image to database through web ui."""
# stop early if possible
if content_list is None or filename_list is None:
logging.info('nothing to upload.')
return None, 'nothing to upload'.title()
# build list of visual communication
vis_com_list = []
for content, filename in zip(content_list, filename_list):
try:
image = VisualCommunication.decode_image(content=content)
vis_com = VisualCommunication(
name=filename,
image=image,
)
except Exception as exc:
logging.error('failed handling data from file: %s', filename)
logging.debug(exc)
continue
vis_com_list.append(vis_com)
# upsert documents
success = upsert_visual_communication(
collection=collection,
visual_communication_list=vis_com_list,
)
if success:
logging.info('uploaded %s files to database', len(vis_com_list))
return 'successfully uploaded images'.title(), None
logging.error('failed uploading images to database')
return None, 'failed uploading images'.title()
# define callback: cycle visual communication data
@app.callback(
Output('warning-message', 'data', allow_duplicate=True),
Output('vis-com-name', 'data'),
Output('image-container', 'src'),
Output({'type': 'annotation', 'index': ALL}, 'value'),
Input('next-button', 'n_clicks'),
State('vis-com-name', 'data'),
State('image-container', 'src'),
State({'type': 'annotation', 'index': ALL}, 'id'),
State({'type': 'annotation', 'index': ALL}, 'value'),
prevent_initial_call=True,
)
def cycle_vis_com_data(
n_clicks: int,
vis_com_name: str,
image_src: str,
annotation_keys: list,
annotation_values: list,
) -> tuple:
"""Cycle visual communcation data."""
assert isinstance(n_clicks, int)
assert isinstance(vis_com_name, str)
assert isinstance(image_src, str)
assert isinstance(annotation_keys, list)
assert isinstance(annotation_values, list)
# prepare default response
response = [
'',
vis_com_name,
image_src,
annotation_values,
]
# stop early if possible
if n_clicks == 0:
return tuple(response)
# check if visual communication name is set
if len(vis_com_name) > 0:
logging.info('saving annotations to database: %s', vis_com_name)
try:
# extract option keys
annotation_keys = [
elem['index']
for elem in annotation_keys
]
# ensure all options are set
logging.info(annotation_keys)
for option, value in zip(annotation_keys, annotation_values):
if value is None:
raise ValueError(f"{option} is not set")
# prepare data to save
annotation_keys = [
elem.replace(' ', '_')
for elem
in annotation_keys
]
annotation_values = [
elem.replace(' ', '_').lower()
for elem
in annotation_values
]
annotation_map = {
key: value
for key, value
in zip(annotation_keys, annotation_values)
}
# instantiate ModelOutputs object
annotations = ModelData.from_annotations(**annotation_map)
# save data to database
upsert_annotation(
collection=collection,
vis_com_name=vis_com_name,
annotations=annotations,
)
except (ValueError, ValidationError) as exc:
msg = f"failed saving annotation: {exc}"
logging.warning(msg)
response[0] = msg
return tuple(response)
# get new visual communication
logging.info('trying to get new visual communication')
try:
# get data
vis_com = get_visual_communication(
collection=collection,
with_annotation=False,
)
# set variables
vis_com_name = vis_com.name
image_src = vis_com.webencoded_image()
if vis_com.prediction is not None:
# TODO: update to use optional predictions
pass
else:
# reset annotations
annotation_values = [None for elem in annotation_values]
except NoDocumentFoundException:
msg = 'no unannotated data in database'
logging.warning(msg)
response[0] = msg
return tuple(response)
else:
response[1] = vis_com_name
response[2] = image_src
response[3] = annotation_values
logging.info(
'finished getting visual communication: %s', vis_com_name,
)
return tuple(response)
logging.info('initialised app')
return app
+11 -11
View File
@@ -4,17 +4,17 @@ import dash_mantine_components as dmc
from dash import dcc
from dash import html
from core.dto import AngleData
from core.dto import ContactData
from core.dto import DistanceData
from core.dto import FramingData
from core.dto import InformationValueData
from core.dto import ModalityColorData
from core.dto import ModalityDepthData
from core.dto import ModalityLightingData
from core.dto import PointOfViewData
from core.dto import SalienceData
from core.dto import VisualSyntaxData
from shared.dto import AngleData
from shared.dto import ContactData
from shared.dto import DistanceData
from shared.dto import FramingData
from shared.dto import InformationValueData
from shared.dto import ModalityColorData
from shared.dto import ModalityDepthData
from shared.dto import ModalityLightingData
from shared.dto import PointOfViewData
from shared.dto import SalienceData
from shared.dto import VisualSyntaxData
def generate_visual_syntax_options_map():
+2 -2
View File
@@ -6,8 +6,8 @@ from dash import html
stores_element = html.Div(
children=[
dcc.Store(id='alert-message', data=''),
dcc.Store(id='success-message', data=''),
dcc.Store(id='warning-message', data=''),
dcc.Store(id='info-message', data=''),
dcc.Store(id='vis-com-name', data=''),
],
)
+12 -42
View File
@@ -1,53 +1,23 @@
"""Definition of web_ui main script."""
from __future__ import annotations
import logging
import os
from pathlib import Path
from dotenv import load_dotenv
from .app import app
# prepare optional local setup
env_path = Path(__file__).parent.parent.parent / 'local.env'
load_dotenv(env_path)
from .app import init_app
from shared.database import connect
from shared.utils import check_env
from shared.utils import setup_logging
# ensure env vars set
necesasary_var_list = {
'MONGO_HOST',
'MONGO_DB',
'MONGO_COLLECTION',
'MONGO_USER',
'MONGO_PASSWORD',
'DASH_AUTH_USERNAME',
'DASH_AUTH_PASSWORD',
}
for env_var in necesasary_var_list:
# ensure env var set
assert (
env_var in os.environ
), (
f"environment variable not set: {env_var}"
)
check_env()
# setup logging stream handler
FMT = (
'%(asctime)s | '
'%(levelname)s | '
'%(filename)s | '
'%(funcName)s | '
'%(message)s'
)
DATEFMT = '%Y-%m-%d %H:%M:%S'
logging.basicConfig(format=FMT, datefmt=DATEFMT, level=logging.INFO)
setup_logging()
logging.info('initialized app')
# connect to database
collection, db, client = connect()
# initialise application
app = init_app(collection=collection)
server = app.server
server.config.update(SECRET_KEY=os.urandom(24))
if __name__ == '__main__':
# prepare local env vars
os.environ['MONGO_HOST'] = 'localhost'
# run app
app.run(debug=True)
logging.info('started app')