From ab176899b37494a3d1cc48714459cd2fd0ed375c Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Wed, 21 Feb 2024 21:30:34 +0100 Subject: [PATCH 01/30] removed unused imports --- tests/test_image_download.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_image_download.py b/tests/test_image_download.py index 2e2b860..d45196c 100644 --- a/tests/test_image_download.py +++ b/tests/test_image_download.py @@ -1,7 +1,5 @@ from pathlib import Path from dotenv import load_dotenv -from pymongo import MongoClient -from typing import List import os from src.database import VisualCommunication, connect -- 2.54.0 From 8f5f329b537c0ee3247b3e14655d5cafb4ddc35a Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 21:59:59 +0100 Subject: [PATCH 02/30] moved latout-related files --- src/web/{ => layout}/body.py | 0 src/web/{ => layout}/header.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/web/{ => layout}/body.py (100%) rename src/web/{ => layout}/header.py (100%) diff --git a/src/web/body.py b/src/web/layout/body.py similarity index 100% rename from src/web/body.py rename to src/web/layout/body.py diff --git a/src/web/header.py b/src/web/layout/header.py similarity index 100% rename from src/web/header.py rename to src/web/layout/header.py -- 2.54.0 From 6c3e772517adcd4494231e176d19c9646cc433b6 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 22:38:16 +0100 Subject: [PATCH 03/30] removed unused file --- src/defaults.ini | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/defaults.ini diff --git a/src/defaults.ini b/src/defaults.ini deleted file mode 100644 index da256a1..0000000 --- a/src/defaults.ini +++ /dev/null @@ -1,6 +0,0 @@ -[main] -logger_level=debug - -[discord] -service_name=visual_critical_discourse_analysis -logger_level=warning -- 2.54.0 From e5344e65191aa918f7392315f802fd4a4d3f6afa Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 22:59:44 +0100 Subject: [PATCH 04/30] added layout elements --- src/web/layout/__init__.py | 1 + src/web/layout/alerts.py | 14 ++++ src/web/layout/body.py | 147 +++---------------------------------- src/web/layout/header.py | 30 +++++--- src/web/layout/image.py | 17 +++++ src/web/layout/inputs.py | 22 ++++++ src/web/layout/labels.py | 104 ++++++++++++++++++++++++++ src/web/layout/layout.py | 35 +++++++++ src/web/layout/stores.py | 6 ++ 9 files changed, 229 insertions(+), 147 deletions(-) create mode 100644 src/web/layout/__init__.py create mode 100644 src/web/layout/alerts.py create mode 100644 src/web/layout/image.py create mode 100644 src/web/layout/inputs.py create mode 100644 src/web/layout/labels.py create mode 100644 src/web/layout/layout.py create mode 100644 src/web/layout/stores.py diff --git a/src/web/layout/__init__.py b/src/web/layout/__init__.py new file mode 100644 index 0000000..d2bc031 --- /dev/null +++ b/src/web/layout/__init__.py @@ -0,0 +1 @@ +from .layout import app_layout \ No newline at end of file diff --git a/src/web/layout/alerts.py b/src/web/layout/alerts.py new file mode 100644 index 0000000..07845b5 --- /dev/null +++ b/src/web/layout/alerts.py @@ -0,0 +1,14 @@ +from dash import html, dcc +import dash_bootstrap_components as dbc + + +alerts_element = html.Div([ + dbc.Alert( + children="", + id="alert_element", + dismissable=True, + fade=False, + is_open=False, + ), + dcc.Store(id="alert_message") +]) diff --git a/src/web/layout/body.py b/src/web/layout/body.py index 1e5e1c9..c88bb59 100644 --- a/src/web/layout/body.py +++ b/src/web/layout/body.py @@ -2,142 +2,19 @@ import dash_mantine_components as dmc from dash import dcc, html from typing import List -from src.model_experiential import ExperientialModelOutput -from src.model_interpersonal import ( - ContactModelOutput, - AngleModelOutput, - PointOfViewModelOutput, - DistanceModelOutput, - ModalityLightingModelOutput, - ModalityColorModelOutput, - ModalityDepthModelOutput -) -from src.model_textual import ( - InformationValueModelOutput, - FramingModelOutput, - SalienceModelOutput -) +from .image import image_element +from .inputs import inputs_element -def generate_option_labels(model) -> List[str]: - """Generate presentable list of attributes from an OutputModel.""" - labels = [ - label.replace('_', ' ').title() - for label in model.list_fields() - ] - return labels -def generate_experiential_options_map(): - """Generate map of titles and options for experiential labels.""" - options_map = {} - # add experiential labels - options_map["experiential".title()] = generate_option_labels(ExperientialModelOutput) - return options_map - -def generate_interpersonal_options_map(): - """Generate map of titles and options for interpersonal labels.""" - options_map = {} - # add interpersonal labels - options_map["contact".title()] = generate_option_labels(ContactModelOutput) - options_map["angle".title()] = generate_option_labels(AngleModelOutput) - options_map["point of view".title()] = generate_option_labels(PointOfViewModelOutput) - options_map["distance".title()] = generate_option_labels(DistanceModelOutput) - options_map["modality lighting".title()] = generate_option_labels(ModalityLightingModelOutput) - options_map["modality color".title()] = generate_option_labels(ModalityColorModelOutput) - options_map["modality depth".title()] = generate_option_labels(ModalityDepthModelOutput) - return options_map - -def generate_textual_options_map(): - """Generate map of titles and options for textual labels.""" - options_map = {} - # add textual labels - options_map["information value".title()] = generate_option_labels(InformationValueModelOutput) - options_map["framing".title()] = generate_option_labels(FramingModelOutput) - options_map["salience".title()] = generate_option_labels(SalienceModelOutput) - return options_map - -def generate_body(): - image_container = dmc.Image( - width=600, - height=600, - withPlaceholder=True, - placeholder=[dmc.Loader(color="gray", size="md")], - ) - # prepare experiential container - experiential_map = generate_experiential_options_map() - experiential_container = dmc.Col( - children=[ - dmc.Container([ - html.H4(list(experiential_map.keys())[0]), - html.B("visual syntax".title()), - dcc.RadioItems(options=list(experiential_map.values())[0]), - ]) - ], span=4 - ) - # prepare interpersonal container - interpersonal_map = generate_interpersonal_options_map() - interpersonal_container = dmc.Col( - children=[ - html.H4("interpersonal".title()), - ], span=4 - ) - for title, options in interpersonal_map.items(): - interpersonal_container.children.append( - dmc.Container([ - html.B(title), - dcc.RadioItems(options) - ]) - ) - # prepare textual container - textual_map = generate_textual_options_map() - textual_container = dmc.Col( - children=[ - html.H4("textual".title()), - ], span=4 - ) - for title, options in textual_map.items(): - textual_container.children.append( - dmc.Container([ - html.B(title), - dcc.RadioItems(options) - ]) - ) - # prepare labels container - label_container = dmc.Grid( - children=[ - experiential_container, - interpersonal_container, - textual_container, - ], - ) - # build the full body container - body_container = dmc.Container( +body_element = dmc.Container( + fluid=True, + children=[ dmc.Grid( + grow=True, children=[ - dmc.Col( - dmc.Center( - image_container, - ), - span=5, - ), - dmc.Col( - # radio buttons part - children = [ - label_container, - dmc.Button( - "confirm", - id="submit-button", - fullWidth=True, - color="lime", - radius="sm", - size="md", - style={ - "height": "50px" - } - ), - ], span=7, - ), - # dmc.Col(span=1), - ], grow=True - ), fluid=True - ) - return body_container \ No newline at end of file + dmc.Col([image_element], span=5), + dmc.Col([inputs_element], span=7) + ], + ) + ], +) \ No newline at end of file diff --git a/src/web/layout/header.py b/src/web/layout/header.py index e1231c4..9e5b2cf 100644 --- a/src/web/layout/header.py +++ b/src/web/layout/header.py @@ -1,15 +1,21 @@ import dash_mantine_components as dmc from dash import html -def generate_header(): - header = dmc.Header( - height=80, - children=[ - dmc.Container( - children=[ - html.H2(children="Visual Critical Discourse Analysis Tool", style={"margin-left": "20px", "padding-top": "-5px"}), - ], size="xl", px="xl", - ), - ] - ) - return header \ No newline at end of file + +header_element = dmc.Header( + height=80, + children=[ + dmc.Container( + children=[ + html.H2( + children="Visual Critical Discourse Analysis Tool", + style={ + "margin-left": "20px", + "padding-top": "-5px" + }, + ), + ], + size="xl", px="xl", + ), + ] +) diff --git a/src/web/layout/image.py b/src/web/layout/image.py new file mode 100644 index 0000000..fa8ae21 --- /dev/null +++ b/src/web/layout/image.py @@ -0,0 +1,17 @@ +import dash_mantine_components as dmc + + +image_element = dmc.Center( + dmc.Image( + id="image_container", + width=600, + height=600, + withPlaceholder=True, + placeholder=[ + dmc.Loader( + color="gray", + size="md" + ) + ] + ) +) \ No newline at end of file diff --git a/src/web/layout/inputs.py b/src/web/layout/inputs.py new file mode 100644 index 0000000..e215ad5 --- /dev/null +++ b/src/web/layout/inputs.py @@ -0,0 +1,22 @@ +import dash_mantine_components as dmc + +from .labels import labels_element + +next_button = dmc.Button( + "next".title(), + id="next-button", + fullWidth=True, + color="lime", + radius="sm", + size="md", + style={ + "height": "50px" + } +) + +inputs_element = dmc.SimpleGrid( + children=[ + labels_element, + next_button + ] +) \ No newline at end of file diff --git a/src/web/layout/labels.py b/src/web/layout/labels.py new file mode 100644 index 0000000..54ce658 --- /dev/null +++ b/src/web/layout/labels.py @@ -0,0 +1,104 @@ +from dash import html, dcc +import dash_mantine_components as dmc +from typing import List + +from src.model_experiential import ExperientialModelOutput +from src.model_interpersonal import ( + ContactModelOutput, + AngleModelOutput, + PointOfViewModelOutput, + DistanceModelOutput, + ModalityLightingModelOutput, + ModalityColorModelOutput, + ModalityDepthModelOutput +) +from src.model_textual import ( + InformationValueModelOutput, + FramingModelOutput, + SalienceModelOutput +) + +def generate_option_labels(model) -> List[str]: + """Generate presentable list of attributes from an OutputModel.""" + labels = [ + label.replace('_', ' ').title() + for label in model.list_fields() + ] + return labels + +def generate_experiential_options_map(): + """Generate map of titles and options for experiential labels.""" + options_map = {} + # add experiential labels + options_map["experiential".title()] = generate_option_labels(ExperientialModelOutput) + return options_map + +def generate_interpersonal_options_map(): + """Generate map of titles and options for interpersonal labels.""" + options_map = {} + # add interpersonal labels + options_map["contact".title()] = generate_option_labels(ContactModelOutput) + options_map["angle".title()] = generate_option_labels(AngleModelOutput) + options_map["point of view".title()] = generate_option_labels(PointOfViewModelOutput) + options_map["distance".title()] = generate_option_labels(DistanceModelOutput) + options_map["modality lighting".title()] = generate_option_labels(ModalityLightingModelOutput) + options_map["modality color".title()] = generate_option_labels(ModalityColorModelOutput) + options_map["modality depth".title()] = generate_option_labels(ModalityDepthModelOutput) + return options_map + +def generate_textual_options_map(): + """Generate map of titles and options for textual labels.""" + options_map = {} + # add textual labels + options_map["information value".title()] = generate_option_labels(InformationValueModelOutput) + options_map["framing".title()] = generate_option_labels(FramingModelOutput) + options_map["salience".title()] = generate_option_labels(SalienceModelOutput) + return options_map + +# prepare experiential container +experiential_map = generate_experiential_options_map() +experiential_container = dmc.Col( + children=[ + dmc.Container([ + html.H4(list(experiential_map.keys())[0]), + html.B("visual syntax".title()), + dcc.RadioItems(options=list(experiential_map.values())[0]), + ]) + ], span=5 +) +# prepare interpersonal container +interpersonal_map = generate_interpersonal_options_map() +interpersonal_container = dmc.Col( + children=[ + html.H4("interpersonal".title()), + ], span=3 +) +for title, options in interpersonal_map.items(): + interpersonal_container.children.append( + dmc.Container([ + html.B(title), + dcc.RadioItems(options) + ]) + ) +# prepare textual container +textual_map = generate_textual_options_map() +textual_container = dmc.Col( + children=[ + html.H4("textual".title()), + ], span=4 +) +for title, options in textual_map.items(): + textual_container.children.append( + dmc.Container([ + html.B(title), + dcc.RadioItems(options) + ]) + ) + +labels_element = dmc.Grid( + children=[ + experiential_container, + interpersonal_container, + textual_container, + ] +) \ No newline at end of file diff --git a/src/web/layout/layout.py b/src/web/layout/layout.py new file mode 100644 index 0000000..7533366 --- /dev/null +++ b/src/web/layout/layout.py @@ -0,0 +1,35 @@ +from dash import dcc +import dash_mantine_components as dmc + +from .stores import stores_element +from .alerts import alerts_element +from .header import header_element +from .body import body_element + + +app_layout = dmc.MantineProvider( + theme={ + "fontFamily": '"Inter", sans-serif', + "components": { + "NavLink": { + "styles": { + "label": { + "color": "#c2c7d0" + } + } + } + }, + }, + children=[ + stores_element, + alerts_element, + dmc.Container( + children=[ + header_element, + body_element, + ], + fluid=True + ), + + ] +) diff --git a/src/web/layout/stores.py b/src/web/layout/stores.py new file mode 100644 index 0000000..6b2cad8 --- /dev/null +++ b/src/web/layout/stores.py @@ -0,0 +1,6 @@ +from dash import html, dcc + + +stores_element = html.Div([ + dcc.Store(id='visual-communication-name', storage_type='session'), +]) \ No newline at end of file -- 2.54.0 From b2c4f97b9c66641f27227321013ddcb1a3539bbf Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 23:26:19 +0100 Subject: [PATCH 05/30] updated id strings --- src/web/layout/alerts.py | 21 +++++++++++---------- src/web/layout/image.py | 2 +- src/web/layout/stores.py | 9 ++++++--- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/web/layout/alerts.py b/src/web/layout/alerts.py index 07845b5..3eaa00f 100644 --- a/src/web/layout/alerts.py +++ b/src/web/layout/alerts.py @@ -2,13 +2,14 @@ from dash import html, dcc import dash_bootstrap_components as dbc -alerts_element = html.Div([ - dbc.Alert( - children="", - id="alert_element", - dismissable=True, - fade=False, - is_open=False, - ), - dcc.Store(id="alert_message") -]) +alerts_element = html.Div( + children=[ + dbc.Alert( + children="", + id="alert-element", + dismissable=True, + fade=False, + is_open=False, + ) + ] +) diff --git a/src/web/layout/image.py b/src/web/layout/image.py index fa8ae21..893a01f 100644 --- a/src/web/layout/image.py +++ b/src/web/layout/image.py @@ -3,7 +3,7 @@ import dash_mantine_components as dmc image_element = dmc.Center( dmc.Image( - id="image_container", + id="image-container", width=600, height=600, withPlaceholder=True, diff --git a/src/web/layout/stores.py b/src/web/layout/stores.py index 6b2cad8..40f40e9 100644 --- a/src/web/layout/stores.py +++ b/src/web/layout/stores.py @@ -1,6 +1,9 @@ from dash import html, dcc -stores_element = html.Div([ - dcc.Store(id='visual-communication-name', storage_type='session'), -]) \ No newline at end of file +stores_element = html.Div( + children=[ + dcc.Store(id="alert-message", storage_type="session"), + dcc.Store(id="visual-communication-name", storage_type="session"), + ] +) \ No newline at end of file -- 2.54.0 From 19d62eb5f28eb016e39e8c00e4b6cfd9db241338 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 23:27:09 +0100 Subject: [PATCH 06/30] updated logging --- src/database/database.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/database/database.py b/src/database/database.py index 692eb70..8f10ac8 100644 --- a/src/database/database.py +++ b/src/database/database.py @@ -1,7 +1,9 @@ from pymongo import MongoClient from dotenv import load_dotenv +import logging import os + def connect(): """Connect to MongoDB.""" # load env vars @@ -18,4 +20,5 @@ def connect(): db = client[os.getenv("MONGO_DB")] collection = db[os.getenv("MONGO_COLLECTION")] collection.create_index("name", unique=True) + logging.info("connected to database") return collection, db, client -- 2.54.0 From 59c648888d028153a6b3e2027137658d81eb9958 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 23:27:46 +0100 Subject: [PATCH 07/30] added function to encode image for webview --- src/database/classes.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/database/classes.py b/src/database/classes.py index 39aad3a..1a88dbb 100644 --- a/src/database/classes.py +++ b/src/database/classes.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, field_validator, field_serializer from PIL import Image from io import BytesIO from pathlib import Path +from base64 import b64encode from src.model_experiential import ExperientialModelOutput from src.model_interpersonal import ( @@ -36,7 +37,7 @@ class ModelOutputs(BaseModel): class VisualCommunication(BaseModel): name: str - image: Image.Image | BytesIO | bytes + image: Image.Image annotation: ModelOutputs | None = None prediction: ModelOutputs | None = None @@ -73,3 +74,15 @@ class VisualCommunication(BaseModel): def __repr__(self) -> str: return f"{self.classname()}(name='{self.name}')" + + def webencoded_image(self) -> str: + """Convert image to be displayed on webpage.""" + # convert images to bytes string + buffer = BytesIO() + self.image.save(buffer, format="png") + img_enc = b64encode(buffer.getvalue()).decode("utf-8") + return img_enc + + +class NoDocumentFoundException(Exception): + pass -- 2.54.0 From a1bf884ff8b799246a6b770894d64b0b6fb8e743 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 23:28:52 +0100 Subject: [PATCH 08/30] added database convenience functions --- src/database/utils.py | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/database/utils.py diff --git a/src/database/utils.py b/src/database/utils.py new file mode 100644 index 0000000..51d8c33 --- /dev/null +++ b/src/database/utils.py @@ -0,0 +1,48 @@ +from pymongo.collection import Collection + +from .classes import ( + VisualCommunication, + NoDocumentFoundException +) + + +def total_documents( + collection: Collection +) -> int: + """Get total number of documents in database.""" + return collection.count_documents(filter={}) + + +def total_annotated( + collection: Collection +) -> int: + """Get total number of annotated documents in database.""" + query = { + "annotation": { "$ne": None } + } + return collection.count_documents(filter=query) + + +def get_visual_communication( + collection: Collection, + with_annotation: bool = False, + with_prediction: bool = False + ) -> VisualCommunication: + """Get a random visual communication from the database.""" + query = {} + if with_annotation: + query["annotation"] = {"$ne": None} + else: + query["annotation"] = None + if with_prediction: + query["prediction"] = {"$ne": None} + else: + query["prediction"] = None + data = collection.aggregate([ + { "$match": query }, # find using filters + { "$sample": { "size": 1 } } # get one random + ]) + data = list(data) # read data from cursor object + if len(data) == 0: + raise NoDocumentFoundException + return VisualCommunication.model_validate(data[0]) -- 2.54.0 From 131edc9913872f1901014026b1d32ecf715899a0 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 23:29:10 +0100 Subject: [PATCH 09/30] exposed new functions --- src/database/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/database/__init__.py b/src/database/__init__.py index 4083b77..e790a76 100644 --- a/src/database/__init__.py +++ b/src/database/__init__.py @@ -1,5 +1,11 @@ from .classes import ( ModelOutputs, - VisualCommunication + VisualCommunication, + NoDocumentFoundException +) +from .database import connect +from .utils import ( + total_documents, + total_annotated, + get_visual_communication ) -from .database import connect \ No newline at end of file -- 2.54.0 From cac816606ac023574cdb0cfc873bd7512f31d9ac Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 23:29:42 +0100 Subject: [PATCH 10/30] updated auto init of app --- src/main.py | 117 +++++++++++++++------------------------------------- 1 file changed, 33 insertions(+), 84 deletions(-) diff --git a/src/main.py b/src/main.py index 7b0b5ea..024b1b5 100644 --- a/src/main.py +++ b/src/main.py @@ -1,99 +1,48 @@ import logging -from discord_logging.handler import DiscordHandler from dotenv import load_dotenv -from configparser import ConfigParser from pathlib import Path import logging import os -from src.web import server +# prepare optional local setup +env_path = Path(__file__).parent.parent / "local.env" +load_dotenv(env_path) -# load default values -config = ConfigParser() -config.read(Path(__file__).parent / 'defaults.ini') -LOGGER_LEVEL = config.get('main', 'logger_level') -DISCORD_SERVICE_NAME = config.get('discord', 'service_name') -DISCORD_LOGGER_LEVEL = config.get('discord', 'logger_level') - - -def setup_logging( - discord_webhook_url: str, - discord_service_name: str = DISCORD_SERVICE_NAME, - discord_logger_level: str = DISCORD_LOGGER_LEVEL, - logger_level: str = LOGGER_LEVEL, -) -> None: - # setup stream handler - fmt = ( - '%(asctime)s | ' - '%(levelname)s | ' - '%(filename)s | ' - '%(funcName)s | ' - '%(message)s' +# ensure env vars set +necesasary_var_list = { + "MONGO_HOST", + "MONGO_DB", + "MONGO_COLLECTION", + "MONGO_USER", + "MONGO_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}" ) - datefmt = '%Y-%m-%d %H:%M:%S' - level = getattr(logging, logger_level.upper()) - logging.basicConfig(format=fmt, datefmt=datefmt, level=level) - logger = logging.getLogger() - # add discord handler - discord_handler = DiscordHandler( - service_name=discord_service_name, - webhook_url=discord_webhook_url, - ) - discord_handler.setFormatter(logging.Formatter('%(message)s')) - level = getattr(logging, discord_logger_level.upper()) - discord_handler.setLevel(level=level) - logger.addHandler(discord_handler) - logging.debug('finished') +# 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) -def initialise_app() -> None: - """ - Ensure all necessary environment variables are provided - """ - # load env vars - load_dotenv() - # ensure env vars set - necesasary_var_map = { - 'LOGGER_LEVEL': False, - 'DISCORD_SERVICE_NAME': False, - 'DISCORD_WEBHOOK_URL': True, - 'DISCORD_LOGGER_LEVEL': False, - 'DATABASE_HOST': True, - 'DATABASE_PORT': True, - 'DATABASE_ENGINE': True, - 'DATABASE_DATABASE': True, - 'DATABASE_USERNAME': True, - 'DATABASE_PASSWORD': True, - } - for env_var, must_be_set in necesasary_var_map.items(): - if must_be_set: - # ensure env var set - assert ( - env_var in os.environ - ), ( - f"environment variable not set: {env_var}" - ) - # set variable from env var - locals()[env_var.lower()] = os.getenv(key=env_var) - else: - # ensure default value set - assert env_var in globals(), f"default variable not set: {env_var}" - # set variable from env var with backup from default value - locals()[env_var.lower()] = os.getenv( - key=env_var, - default=globals()[env_var] - ) - setup_logging( - discord_webhook_url=locals()['discord_webhook_url'], - discord_service_name=locals()['discord_service_name'], - discord_logger_level=locals()['discord_logger_level'], - logger_level=locals()['logger_level'], - ) - logging.debug('finished') +logging.info("initialized app") +from src.web import app +server = app.server if __name__ == "__main__": - from src.web import app - # initialise_app() + # prepare local env vars + os.environ["MONGO_HOST"] = "localhost" + # run app app.run(debug=True) logging.info("started app") \ No newline at end of file -- 2.54.0 From 2ff699856c9834b04a7293c58dcfa1b038b39c87 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 23:29:59 +0100 Subject: [PATCH 11/30] updated and added tests --- tests/test_get_visual_communication.py | 21 +++++++++++++++++++++ tests/test_image_download.py | 1 + tests/test_total_annotated.py | 20 ++++++++++++++++++++ tests/test_total_documents.py | 20 ++++++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 tests/test_get_visual_communication.py create mode 100644 tests/test_total_annotated.py create mode 100644 tests/test_total_documents.py diff --git a/tests/test_get_visual_communication.py b/tests/test_get_visual_communication.py new file mode 100644 index 0000000..57e7c0b --- /dev/null +++ b/tests/test_get_visual_communication.py @@ -0,0 +1,21 @@ +from pathlib import Path +from dotenv import load_dotenv +import os + +from src.database import ( + connect, + get_visual_communication +) + +if __name__ == "__main__": + # prepare env vars + env_path = Path(__file__).parent.parent / "local.env" + assert env_path.exists() + load_dotenv(env_path) + os.environ["MONGO_HOST"] = "localhost" + # connect to database + collection, db, client = connect() + print(client.server_info()) + # get visual communication + vis_com = get_visual_communication(collection) + print(vis_com.image) \ No newline at end of file diff --git a/tests/test_image_download.py b/tests/test_image_download.py index d45196c..bbb6ac6 100644 --- a/tests/test_image_download.py +++ b/tests/test_image_download.py @@ -23,3 +23,4 @@ if __name__ == "__main__": print(repr(vis_com)) if data is not None: print(vis_com.image) + print(vis_com.model_dump()) diff --git a/tests/test_total_annotated.py b/tests/test_total_annotated.py new file mode 100644 index 0000000..3726870 --- /dev/null +++ b/tests/test_total_annotated.py @@ -0,0 +1,20 @@ +from pathlib import Path +from dotenv import load_dotenv +import os + +from src.database import ( + connect, + total_annotated +) + +if __name__ == "__main__": + # prepare env vars + env_path = Path(__file__).parent.parent / "local.env" + assert env_path.exists() + load_dotenv(env_path) + os.environ["MONGO_HOST"] = "localhost" + # connect to database + collection, db, client = connect() + # get visual communication + num_docs = total_annotated(collection) + print(f"number of annotated documents in database: {num_docs}") \ No newline at end of file diff --git a/tests/test_total_documents.py b/tests/test_total_documents.py new file mode 100644 index 0000000..73e92c5 --- /dev/null +++ b/tests/test_total_documents.py @@ -0,0 +1,20 @@ +from pathlib import Path +from dotenv import load_dotenv +import os + +from src.database import ( + connect, + total_documents +) + +if __name__ == "__main__": + # prepare env vars + env_path = Path(__file__).parent.parent / "local.env" + assert env_path.exists() + load_dotenv(env_path) + os.environ["MONGO_HOST"] = "localhost" + # connect to database + collection, db, client = connect() + # get visual communication + num_docs = total_documents(collection) + print(f"total number of documents in database: {num_docs}") \ No newline at end of file -- 2.54.0 From 51c96b36cb2fbd7946560c7ca1b43830d77b79cb Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Fri, 23 Feb 2024 23:30:12 +0100 Subject: [PATCH 12/30] removed unused packages --- poetry.lock | 36 +----------------------------------- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/poetry.lock b/poetry.lock index fa70713..1909a9e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -251,23 +251,6 @@ files = [ {file = "dash_table-5.0.0.tar.gz", hash = "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308"}, ] -[[package]] -name = "discord-webhook" -version = "1.3.1" -description = "Easily send Discord webhooks with Python" -optional = false -python-versions = ">=3.10,<4.0" -files = [ - {file = "discord_webhook-1.3.1-py3-none-any.whl", hash = "sha256:ede07028316de76d24eb811836e2b818b2017510da786777adcb0d5970e7af79"}, - {file = "discord_webhook-1.3.1.tar.gz", hash = "sha256:ee3e0f3ea4f3dc8dc42be91f75b894a01624c6c13fea28e23ebcf9a6c9a304f7"}, -] - -[package.dependencies] -requests = ">=2.28.1,<3.0.0" - -[package.extras] -async = ["httpx (>=0.23.0,<0.24.0)"] - [[package]] name = "dnspython" version = "2.6.1" @@ -806,23 +789,6 @@ files = [ [package.extras] cli = ["click (>=5.0)"] -[[package]] -name = "python-logging-discord-handler" -version = "0.1.4" -description = "Discord handler for Python logging framework" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "python_logging_discord_handler-0.1.4-py3-none-any.whl", hash = "sha256:b804b48e3f5af8c9c781a9afe8243c806f01521662e38a60fcda2c3631d27f4f"}, - {file = "python_logging_discord_handler-0.1.4.tar.gz", hash = "sha256:8bfa839b6503b3b87e5851dd13bc5ff80bf2fadb496ac22c338ac10bd926f75a"}, -] - -[package.dependencies] -discord-webhook = ">=1.0.0,<2.0.0" - -[package.extras] -docs = ["Sphinx (>=4.4.0,<5.0.0)", "sphinx-autodoc-typehints[docs] (>=1.16.0,<2.0.0)", "sphinx-rtd-theme (>=1.0.0,<2.0.0)", "sphinx-sitemap (>=2.2.0,<3.0.0)"] - [[package]] name = "requests" version = "2.31.0" @@ -962,4 +928,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "e4aacea5a98281d935411e0d96152d1d24680f6c1e5288e9a1be913a0536b78e" +content-hash = "9146cd32f0af25ddc99e5950dbf6ebc5a249f16238834b9a5db8d1b45fa9efb9" diff --git a/pyproject.toml b/pyproject.toml index f71a170..880a82d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ packages = [ [tool.poetry.dependencies] python = "^3.12" gunicorn = "^21.2.0" -python-logging-discord-handler = "^0.1.4" python-dotenv = "^1.0.1" dash = "^2.15.0" dash-bootstrap-components = "^1.5.0" -- 2.54.0 From 20de0376b6dbeb8541dc4de148c019114f7670e7 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 00:03:39 +0100 Subject: [PATCH 13/30] added init image --- src/web/layout/image.py | 24 +++++++++++++----------- src/web/layout/init_img.png | Bin 0 -> 9479 bytes 2 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 src/web/layout/init_img.png diff --git a/src/web/layout/image.py b/src/web/layout/image.py index 893a01f..59f2c2a 100644 --- a/src/web/layout/image.py +++ b/src/web/layout/image.py @@ -1,17 +1,19 @@ import dash_mantine_components as dmc +from dash import html +from pathlib import Path +from base64 import b64encode +# read init img +init_img_path = Path(__file__).parent / "init_img.png" +with open(init_img_path.absolute(), "rb") as fh: + init_img_enc = b64encode(fh.read()).decode("utf-8") image_element = dmc.Center( - dmc.Image( + html.Img( + style={ + "width": "100%", + }, id="image-container", - width=600, - height=600, - withPlaceholder=True, - placeholder=[ - dmc.Loader( - color="gray", - size="md" - ) - ] + src=f"data:image/png;base64, {init_img_enc}" ) -) \ No newline at end of file +) diff --git a/src/web/layout/init_img.png b/src/web/layout/init_img.png new file mode 100644 index 0000000000000000000000000000000000000000..7d821dfcd976138a85122865a919288bf9b54707 GIT binary patch literal 9479 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 Date: Sat, 24 Feb 2024 00:04:34 +0100 Subject: [PATCH 14/30] restructured app --- src/web/app.py | 79 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/src/web/app.py b/src/web/app.py index eac840a..6cc300b 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -1,28 +1,65 @@ -from dash import Dash, html, Input, Output, State, page_container, page_registry +from dash import Dash, Input, Output import dash_bootstrap_components as dbc -import dash_mantine_components as dmc - -from .header import generate_header -from .body import generate_body +import logging +from .layout import app_layout +from src.database import ( + connect, + get_visual_communication, + NoDocumentFoundException +) +# setup app app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) +app.title = "visual critical discourse analysis".title() +app.layout = app_layout server = app.server -app.layout = dmc.MantineProvider( - theme={ - 'fontFamily': '"Inter", sans-serif', - "components": { - "NavLink":{'styles':{'label':{'color':'#c2c7d0'}}} - }, - }, - children=[ - dmc.Container( - [ - generate_header(), - generate_body(), - ], fluid=True - ), - ], +# connect to database +collection, db, client = connect() + +# define callbacks +@app.callback( + Output("alert-element", "is_open"), + Output("alert-element", "children"), + Input("alert-message", "data") ) - \ No newline at end of file +def show_alert( + msg: str | None +): + if msg is None: + return False, "" + return True, msg + +@app.callback( + Output("alert-message", "data"), + Output("visual-communication-name", "data"), + Output("image-container", "src"), + Input("next-button", "n_clicks"), + prevent_initial_call=True +) +def load_unannotated_visual_communication_data( + n_clicks: int +): + global collection + try: + vis_com = get_visual_communication( + collection=collection, + with_annotation=False + ) + except NoDocumentFoundException: + return ( + "Did not find any unannotated data in database", + None, + "" + ) + # prepare return values + alert_message = None + vis_com_name = vis_com.name + img_src = f"data:image/png;base64, {vis_com.webencoded_image()}" + logging.info("updated visual communication") + return ( + alert_message, + vis_com_name, + img_src + ) -- 2.54.0 From 81966289816bc55002f43a4e10d58ac8886c6031 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 11:58:00 +0100 Subject: [PATCH 15/30] added function to randomly generate prediction data --- src/database/classes.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/database/classes.py b/src/database/classes.py index 1a88dbb..568b415 100644 --- a/src/database/classes.py +++ b/src/database/classes.py @@ -4,6 +4,9 @@ from PIL import Image from io import BytesIO from pathlib import Path from base64 import b64encode +import random +import logging +from typing import List from src.model_experiential import ExperientialModelOutput from src.model_interpersonal import ( @@ -34,6 +37,21 @@ class ModelOutputs(BaseModel): framing: FramingModelOutput salience: SalienceModelOutput + @classmethod + def list_fields(cls) -> List[str]: + """List options that are stored as attributes.""" + return list(cls.model_fields.keys()) + + @classmethod + def from_random(cls) -> ModelOutputs: + """Instantiate with random numbers.""" + kwargs = { + field: field_info.annotation.from_random() + for field, field_info + in cls.model_fields.items() + } + return cls(**kwargs) + class VisualCommunication(BaseModel): name: str @@ -83,6 +101,11 @@ class VisualCommunication(BaseModel): img_enc = b64encode(buffer.getvalue()).decode("utf-8") return img_enc + def generate_random_prediction(self, force: bool = False) -> None: + """Generate random prediction values.""" + if not force and self.prediction is not None: + logging.warning("set force=True to overwrite existing values.") + self.prediction = ModelOutputs.from_random() class NoDocumentFoundException(Exception): pass -- 2.54.0 From 91d02f1ecb012c75c5628d0c106ca3904b9cc7b0 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 11:58:37 +0100 Subject: [PATCH 16/30] tested random prediction generation --- tests/test_generate_random_prediction.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/test_generate_random_prediction.py diff --git a/tests/test_generate_random_prediction.py b/tests/test_generate_random_prediction.py new file mode 100644 index 0000000..f2a2845 --- /dev/null +++ b/tests/test_generate_random_prediction.py @@ -0,0 +1,17 @@ +from pathlib import Path + +from src.database import VisualCommunication + + +if __name__ == "__main__": + # get list of image paths + test_dir = Path(__file__).parent + img_dir = test_dir / "imgs" + img_path_list = [path for path in img_dir.glob("*.jpeg") if path.is_file()] + print(img_path_list) + # instantiate data object + vis_com_list = [VisualCommunication.from_file(path) for path in img_path_list] + # generate random predictions + [vis_com.generate_random_prediction() for vis_com in vis_com_list] + for vis_com in vis_com_list: + print(vis_com) \ No newline at end of file -- 2.54.0 From e82a03f298e3c8c8c98fa53f391d432f649e65d0 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 20:10:10 +0100 Subject: [PATCH 17/30] updated container name --- docker-compose.local.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 247052c..e6dcf17 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -25,6 +25,7 @@ services: - backend mongo-express: image: mongo-express + container_name: mongo_express ports: - 8081:8081 env_file: -- 2.54.0 From 7b0fb5ef5d5218af3b52161ab3aeea5fdbf2ebcf Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 21:50:53 +0100 Subject: [PATCH 18/30] added dynamic ids --- src/web/layout/labels.py | 63 +++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/web/layout/labels.py b/src/web/layout/labels.py index 54ce658..12d3655 100644 --- a/src/web/layout/labels.py +++ b/src/web/layout/labels.py @@ -2,7 +2,9 @@ from dash import html, dcc import dash_mantine_components as dmc from typing import List -from src.model_experiential import ExperientialModelOutput +from src.model_experiential import ( + VisualSyntaxModelOutput +) from src.model_interpersonal import ( ContactModelOutput, AngleModelOutput, @@ -26,46 +28,53 @@ def generate_option_labels(model) -> List[str]: ] return labels -def generate_experiential_options_map(): - """Generate map of titles and options for experiential labels.""" +def generate_visual_syntax_options_map(): + """Generate map of titles and options for visual syntax labels.""" options_map = {} # add experiential labels - options_map["experiential".title()] = generate_option_labels(ExperientialModelOutput) + options_map["visual syntax"] = VisualSyntaxModelOutput.list_fields() return options_map def generate_interpersonal_options_map(): """Generate map of titles and options for interpersonal labels.""" options_map = {} # add interpersonal labels - options_map["contact".title()] = generate_option_labels(ContactModelOutput) - options_map["angle".title()] = generate_option_labels(AngleModelOutput) - options_map["point of view".title()] = generate_option_labels(PointOfViewModelOutput) - options_map["distance".title()] = generate_option_labels(DistanceModelOutput) - options_map["modality lighting".title()] = generate_option_labels(ModalityLightingModelOutput) - options_map["modality color".title()] = generate_option_labels(ModalityColorModelOutput) - options_map["modality depth".title()] = generate_option_labels(ModalityDepthModelOutput) + options_map["contact"] = generate_option_labels(ContactModelOutput) + options_map["angle"] = generate_option_labels(AngleModelOutput) + options_map["point of view"] = generate_option_labels(PointOfViewModelOutput) + options_map["distance"] = generate_option_labels(DistanceModelOutput) + options_map["modality lighting"] = generate_option_labels(ModalityLightingModelOutput) + options_map["modality color"] = generate_option_labels(ModalityColorModelOutput) + options_map["modality depth"] = generate_option_labels(ModalityDepthModelOutput) return options_map def generate_textual_options_map(): """Generate map of titles and options for textual labels.""" options_map = {} # add textual labels - options_map["information value".title()] = generate_option_labels(InformationValueModelOutput) - options_map["framing".title()] = generate_option_labels(FramingModelOutput) - options_map["salience".title()] = generate_option_labels(SalienceModelOutput) + options_map["information value"] = generate_option_labels(InformationValueModelOutput) + options_map["framing"] = generate_option_labels(FramingModelOutput) + options_map["salience"] = generate_option_labels(SalienceModelOutput) return options_map # prepare experiential container -experiential_map = generate_experiential_options_map() +experiential_map = generate_visual_syntax_options_map() experiential_container = dmc.Col( children=[ - dmc.Container([ - html.H4(list(experiential_map.keys())[0]), - html.B("visual syntax".title()), - dcc.RadioItems(options=list(experiential_map.values())[0]), - ]) + html.H4("experiential".title()), ], span=5 ) +for title, options in experiential_map.items(): + id_dict = {"type": "annotation", "index": title.replace('_', '-')} + experiential_container.children.append( + dmc.Container([ + html.B(title.title()), + dcc.RadioItems( + options=options, + id=id_dict, + ), + ]) + ) # prepare interpersonal container interpersonal_map = generate_interpersonal_options_map() interpersonal_container = dmc.Col( @@ -74,10 +83,14 @@ interpersonal_container = dmc.Col( ], span=3 ) for title, options in interpersonal_map.items(): + id_dict = {"type": "annotation", "index": title.replace('_', '-')} interpersonal_container.children.append( dmc.Container([ - html.B(title), - dcc.RadioItems(options) + html.B(title.title()), + dcc.RadioItems( + options=options, + id=id_dict, + ), ]) ) # prepare textual container @@ -88,10 +101,14 @@ textual_container = dmc.Col( ], span=4 ) for title, options in textual_map.items(): + id_dict = {"type": "annotation", "index": title.replace('_', '-')} textual_container.children.append( dmc.Container([ html.B(title), - dcc.RadioItems(options) + dcc.RadioItems( + options=options, + id=id_dict, + ), ]) ) -- 2.54.0 From c4f220a61ac18b64ca4bf949478a7108691f8503 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 21:51:37 +0100 Subject: [PATCH 19/30] updated logging and added convenience functions --- src/database/utils.py | 60 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/src/database/utils.py b/src/database/utils.py index 51d8c33..97db200 100644 --- a/src/database/utils.py +++ b/src/database/utils.py @@ -1,8 +1,10 @@ from pymongo.collection import Collection +import logging from .classes import ( VisualCommunication, - NoDocumentFoundException + NoDocumentFoundException, + ModelOutputs ) @@ -25,8 +27,7 @@ def total_annotated( def get_visual_communication( collection: Collection, - with_annotation: bool = False, - with_prediction: bool = False + with_annotation: bool = False ) -> VisualCommunication: """Get a random visual communication from the database.""" query = {} @@ -34,15 +35,56 @@ def get_visual_communication( query["annotation"] = {"$ne": None} else: query["annotation"] = None - if with_prediction: - query["prediction"] = {"$ne": None} - else: - query["prediction"] = None data = collection.aggregate([ { "$match": query }, # find using filters { "$sample": { "size": 1 } } # get one random ]) data = list(data) # read data from cursor object if len(data) == 0: - raise NoDocumentFoundException - return VisualCommunication.model_validate(data[0]) + logging.error("failed getting visual communication") + raise NoDocumentFoundException() + data = data[0] + logging.info("finished") + return VisualCommunication.model_validate(data) + + +def upsert_predictions( + collection: Collection, + vis_com_name: str, + predictions: ModelOutputs, +) -> None: + """Upsert prediction data in the database.""" + query = { + "name": vis_com_name + } + update = { + "$set": { "prediction": predictions.model_dump() } + } + res = collection.update_one( + filter=query, + update=update, + upsert=True, + ) + logging.debug("upserted document: %s", res) + logging.info("finished") + + +def upsert_annotations( + collection: Collection, + vis_com_name: str, + annotations: ModelOutputs, +) -> None: + """Upserts annotation data in the database.""" + query = { + "name": vis_com_name + } + update = { + "annotation": annotations.model_dump() + } + res = collection.update_one( + filter=query, + update=update, + upsert=True, + ) + logging.info("upserted document: %s", res) + logging.info("finished") -- 2.54.0 From b0ca86677f69d7b30a549772085d85200e233b93 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 21:51:56 +0100 Subject: [PATCH 20/30] exposed new functions --- src/database/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/database/__init__.py b/src/database/__init__.py index e790a76..3cf16e2 100644 --- a/src/database/__init__.py +++ b/src/database/__init__.py @@ -7,5 +7,7 @@ from .database import connect from .utils import ( total_documents, total_annotated, - get_visual_communication + get_visual_communication, + upsert_annotations, + upsert_predictions, ) -- 2.54.0 From 8389a20745630e532c5e90f8d86a12567fafa5ed Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 21:52:19 +0100 Subject: [PATCH 21/30] added dev env var --- docker-compose.local.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.local.yml b/docker-compose.local.yml index e6dcf17..5c237a1 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -8,6 +8,8 @@ services: dockerfile: Dockerfile env_file: - local.env + environment: + - ENV=DEV ports: - 8050:8050 networks: -- 2.54.0 From e8ba277dab7db72860981b8c4255de384d3f19c3 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 21:53:36 +0100 Subject: [PATCH 22/30] elaborated on init str generation --- src/web/layout/image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/web/layout/image.py b/src/web/layout/image.py index 59f2c2a..a32819a 100644 --- a/src/web/layout/image.py +++ b/src/web/layout/image.py @@ -7,6 +7,8 @@ from base64 import b64encode init_img_path = Path(__file__).parent / "init_img.png" with open(init_img_path.absolute(), "rb") as fh: init_img_enc = b64encode(fh.read()).decode("utf-8") +# generate init img string +init_img_src = f"data:image/png;base64, {init_img_enc}" image_element = dmc.Center( html.Img( @@ -14,6 +16,6 @@ image_element = dmc.Center( "width": "100%", }, id="image-container", - src=f"data:image/png;base64, {init_img_enc}" + src=init_img_src ) ) -- 2.54.0 From e7d91a488624e7746cad7bef34a646ec03549568 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 21:53:59 +0100 Subject: [PATCH 23/30] added n_clicks default to button --- src/web/layout/inputs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/web/layout/inputs.py b/src/web/layout/inputs.py index e215ad5..5e37adc 100644 --- a/src/web/layout/inputs.py +++ b/src/web/layout/inputs.py @@ -5,6 +5,7 @@ from .labels import labels_element next_button = dmc.Button( "next".title(), id="next-button", + n_clicks=0, fullWidth=True, color="lime", radius="sm", -- 2.54.0 From 47cae849ff9dd8a30a31991d75a7ee8264e0a37e Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 21:54:36 +0100 Subject: [PATCH 24/30] added env dependent storage type --- src/web/layout/stores.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/web/layout/stores.py b/src/web/layout/stores.py index 40f40e9..7345d8b 100644 --- a/src/web/layout/stores.py +++ b/src/web/layout/stores.py @@ -1,9 +1,16 @@ from dash import html, dcc +import logging +import os +storage_type = "session" +if "ENV" in os.environ and os.getenv("ENV") == "DEV": + storage_type = "memory" + logging.info(f"ENV=DEV -> dcc.Stores changed to storage_type={storage_type}") + stores_element = html.Div( children=[ - dcc.Store(id="alert-message", storage_type="session"), - dcc.Store(id="visual-communication-name", storage_type="session"), + dcc.Store(id="alert-message", storage_type=storage_type, data=""), + dcc.Store(id="vis-com-name", storage_type=storage_type, data=""), ] ) \ No newline at end of file -- 2.54.0 From 59ad0104ad84668106b3b41cd5bd249c9e4d556b Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 22:41:27 +0100 Subject: [PATCH 25/30] fixed bug when upserting annotations --- src/database/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/utils.py b/src/database/utils.py index 97db200..9a08025 100644 --- a/src/database/utils.py +++ b/src/database/utils.py @@ -79,7 +79,7 @@ def upsert_annotations( "name": vis_com_name } update = { - "annotation": annotations.model_dump() + "$set": { "annotation": annotations.model_dump() } } res = collection.update_one( filter=query, -- 2.54.0 From 5a147e949295ab65f984a61998de56863a30480b Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 22:42:11 +0100 Subject: [PATCH 26/30] added from_choice classmethod --- src/model_experiential/__init__.py | 2 +- src/model_experiential/classes.py | 21 ++++++++++++++++++--- src/model_interpersonal/classes.py | 14 +++++++++++++- src/model_textual/classes.py | 14 +++++++++++++- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/model_experiential/__init__.py b/src/model_experiential/__init__.py index 9a2791c..7842596 100644 --- a/src/model_experiential/__init__.py +++ b/src/model_experiential/__init__.py @@ -1,4 +1,4 @@ -from .classes import ExperientialModelOutput +from .classes import VisualSyntaxModelOutput diff --git a/src/model_experiential/classes.py b/src/model_experiential/classes.py index 29f550e..cd8e9e0 100644 --- a/src/model_experiential/classes.py +++ b/src/model_experiential/classes.py @@ -1,8 +1,11 @@ -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from typing import List import random +class OptionNotSetException(Exception): + pass + class ModelOutput(BaseModel): @classmethod @@ -20,6 +23,18 @@ class ModelOutput(BaseModel): """Instantiate with random numbers.""" kwargs = {field: random.random() for field in cls.list_fields()} return cls(**kwargs) + + @classmethod + def from_choice(cls, option: str): + """Instantiate from choice.""" + if option is None: + raise ValidationError() + assert isinstance(option, str), "option is not a string" + allowed_options_list = cls.list_fields() + assert option in allowed_options_list, f"{option} is not among allowed fields {allowed_options_list}" + kwargs = {field: 0 for field in cls.list_fields()} + kwargs[option] = 1 + return cls(**kwargs) def __repr__(self) -> str: model_dict = self.model_dump() @@ -39,7 +54,7 @@ class ModelOutput(BaseModel): return max(model_dict.values()) -class ExperientialModelOutput(ModelOutput): +class VisualSyntaxModelOutput(ModelOutput): non_transactional_action: float non_transactional_reaction: float unidirectional_transactional_action: float @@ -61,7 +76,7 @@ class ExperientialModelOutput(ModelOutput): if __name__ == '__main__': - m = ExperientialModelOutput.from_random() + m = VisualSyntaxModelOutput.from_random() print(m) print(repr(m)) print(m.highest_score_field()) diff --git a/src/model_interpersonal/classes.py b/src/model_interpersonal/classes.py index d57717d..a702d1a 100644 --- a/src/model_interpersonal/classes.py +++ b/src/model_interpersonal/classes.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from typing import List import random @@ -20,6 +20,18 @@ class ModelOutput(BaseModel): """Instantiate with random numbers.""" kwargs = {field: random.random() for field in cls.list_fields()} return cls(**kwargs) + + @classmethod + def from_choice(cls, option: str): + """Instantiate from choice.""" + if option is None: + raise ValidationError() + assert isinstance(option, str) + allowed_options_list = cls.list_fields() + assert option in allowed_options_list, f"{option} is not among allowed fields {allowed_options_list}" + kwargs = {field: 0 for field in cls.list_fields()} + kwargs[option] = 1 + return cls(**kwargs) def __repr__(self) -> str: model_dict = self.model_dump() diff --git a/src/model_textual/classes.py b/src/model_textual/classes.py index a9bc4d1..bab0672 100644 --- a/src/model_textual/classes.py +++ b/src/model_textual/classes.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from typing import List import random @@ -20,6 +20,18 @@ class ModelOutput(BaseModel): """Instantiate with random numbers.""" kwargs = {field: random.random() for field in cls.list_fields()} return cls(**kwargs) + + @classmethod + def from_choice(cls, option: str): + """Instantiate from choice.""" + if option is None: + raise ValidationError() + assert isinstance(option, str) + allowed_options_list = cls.list_fields() + assert option in allowed_options_list, f"{option} is not among allowed fields {allowed_options_list}" + kwargs = {field: 0 for field in cls.list_fields()} + kwargs[option] = 1 + return cls(**kwargs) def __repr__(self) -> str: model_dict = self.model_dump() -- 2.54.0 From 3fb2fb62587d6bf1c17a176659068137ed946636 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 22:42:44 +0100 Subject: [PATCH 27/30] fixed bug when generating option labels --- src/web/layout/labels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/layout/labels.py b/src/web/layout/labels.py index 12d3655..f8a90bd 100644 --- a/src/web/layout/labels.py +++ b/src/web/layout/labels.py @@ -32,7 +32,7 @@ def generate_visual_syntax_options_map(): """Generate map of titles and options for visual syntax labels.""" options_map = {} # add experiential labels - options_map["visual syntax"] = VisualSyntaxModelOutput.list_fields() + options_map["visual syntax"] = generate_option_labels(VisualSyntaxModelOutput) return options_map def generate_interpersonal_options_map(): -- 2.54.0 From d3eac227068885bfa1b6b50c62ace05b9521f160 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 22:43:26 +0100 Subject: [PATCH 28/30] added from_annotations classmethod and fixed bug when generating webencoded image --- src/database/classes.py | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/database/classes.py b/src/database/classes.py index 568b415..06387a0 100644 --- a/src/database/classes.py +++ b/src/database/classes.py @@ -4,11 +4,12 @@ from PIL import Image from io import BytesIO from pathlib import Path from base64 import b64encode -import random import logging from typing import List -from src.model_experiential import ExperientialModelOutput +from src.model_experiential import ( + VisualSyntaxModelOutput +) from src.model_interpersonal import ( ContactModelOutput, AngleModelOutput, @@ -25,7 +26,7 @@ from src.model_textual import ( ) class ModelOutputs(BaseModel): - experiential: ExperientialModelOutput + visual_syntax: VisualSyntaxModelOutput contact: ContactModelOutput angle: AngleModelOutput point_of_view: PointOfViewModelOutput @@ -51,6 +52,37 @@ class ModelOutputs(BaseModel): in cls.model_fields.items() } return cls(**kwargs) + + @classmethod + def from_annotations( + cls, + visual_syntax: str, + contact: str, + angle: str, + point_of_view: str, + distance: str, + modality_lighting: str, + modality_color: str, + modality_depth: str, + information_value: str, + framing: str, + salience: str + ) -> ModelOutputs: + """Instantiate from annotation.""" + kwargs = { + "visual_syntax": VisualSyntaxModelOutput.from_choice(visual_syntax), + "contact": ContactModelOutput.from_choice(contact), + "angle": AngleModelOutput.from_choice(angle), + "point_of_view": PointOfViewModelOutput.from_choice(point_of_view), + "distance": DistanceModelOutput.from_choice(distance), + "modality_lighting": ModalityLightingModelOutput.from_choice(modality_lighting), + "modality_color": ModalityColorModelOutput.from_choice(modality_color), + "modality_depth": ModalityDepthModelOutput.from_choice(modality_depth), + "information_value": InformationValueModelOutput.from_choice(information_value), + "framing": FramingModelOutput.from_choice(framing), + "salience": SalienceModelOutput.from_choice(salience) + } + return cls(**kwargs) class VisualCommunication(BaseModel): @@ -99,7 +131,7 @@ class VisualCommunication(BaseModel): buffer = BytesIO() self.image.save(buffer, format="png") img_enc = b64encode(buffer.getvalue()).decode("utf-8") - return img_enc + return f"data:image/png;base64, {img_enc}" def generate_random_prediction(self, force: bool = False) -> None: """Generate random prediction values.""" -- 2.54.0 From c3ad732448ee269941854cbcd84cde81aaba5500 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 22:43:56 +0100 Subject: [PATCH 29/30] updated tests --- tests/test_get_visual_communication.py | 2 +- tests/test_model_outputs_from_annotation.py | 13 ++++++ tests/test_prediction_upload.py | 44 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/test_model_outputs_from_annotation.py create mode 100644 tests/test_prediction_upload.py diff --git a/tests/test_get_visual_communication.py b/tests/test_get_visual_communication.py index 57e7c0b..21787c3 100644 --- a/tests/test_get_visual_communication.py +++ b/tests/test_get_visual_communication.py @@ -18,4 +18,4 @@ if __name__ == "__main__": print(client.server_info()) # get visual communication vis_com = get_visual_communication(collection) - print(vis_com.image) \ No newline at end of file + print(vis_com) diff --git a/tests/test_model_outputs_from_annotation.py b/tests/test_model_outputs_from_annotation.py new file mode 100644 index 0000000..d64b3b5 --- /dev/null +++ b/tests/test_model_outputs_from_annotation.py @@ -0,0 +1,13 @@ +from src.database import ModelOutputs + + +if __name__ == "__main__": + # instantiate data object + annotation = { + + } + vis_com_list = [ModelOutputs.from_annotation(path) for path in img_path_list] + # generate random predictions + [vis_com.generate_random_prediction() for vis_com in vis_com_list] + for vis_com in vis_com_list: + print(vis_com) \ No newline at end of file diff --git a/tests/test_prediction_upload.py b/tests/test_prediction_upload.py new file mode 100644 index 0000000..871ef3f --- /dev/null +++ b/tests/test_prediction_upload.py @@ -0,0 +1,44 @@ +from pathlib import Path +from dotenv import load_dotenv +import os +import logging + +from src.database import ( + VisualCommunication, + connect, + upsert_predictions +) + +if __name__ == "__main__": + # setup logging + 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) + # get list of image paths + test_dir = Path(__file__).parent + img_dir = test_dir / "imgs" + img_path_list = [path for path in img_dir.glob("*.jpeg") if path.is_file()] + # instantiate data object + vis_com_list = [VisualCommunication.from_file(path) for path in img_path_list] + # generate random predictions + [vis_com.generate_random_prediction() for vis_com in vis_com_list] + # prepare env vars + env_path = test_dir.parent / "local.env" + assert env_path.exists() + load_dotenv(env_path) + os.environ["MONGO_HOST"] = "localhost" + # connect to database + collection, db, client = connect() + # upload visual communication + for vis_com in vis_com_list: + upsert_predictions( + collection=collection, + vis_com_name=vis_com.name, + predictions=vis_com.prediction, + ) -- 2.54.0 From 1bc4072e525d417e7f9ac958fd7211ae33eb7005 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Sat, 24 Feb 2024 22:44:28 +0100 Subject: [PATCH 30/30] applied new functions and fixed bugs --- src/web/app.py | 120 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 22 deletions(-) diff --git a/src/web/app.py b/src/web/app.py index 6cc300b..d5bd975 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -1,12 +1,16 @@ -from dash import Dash, Input, Output +from dash import Dash, Input, Output, State, ALL import dash_bootstrap_components as dbc import logging +from typing import List +from pydantic import ValidationError from .layout import app_layout from src.database import ( connect, get_visual_communication, - NoDocumentFoundException + NoDocumentFoundException, + upsert_annotations, + ModelOutputs ) # setup app @@ -27,39 +31,111 @@ collection, db, client = connect() def show_alert( msg: str | None ): - if msg is None: + if msg is None or msg == "": return False, "" + logging.info(f"updated alert message: {msg}") return True, msg @app.callback( Output("alert-message", "data"), - Output("visual-communication-name", "data"), + Output("vis-com-name", "data"), Output("image-container", "src"), + Output({"type": "annotation", "index": ALL}, "value"), Input("next-button", "n_clicks"), - prevent_initial_call=True + 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 load_unannotated_visual_communication_data( - n_clicks: int +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 + ] + annotations = { + key: value + for key, value + in zip(annotation_keys, annotation_values) + } + # instantiate ModelOutputs object + annotations = ModelOutputs.from_annotations(**annotations) + # save data to + upsert_annotations( + 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: - return ( - "Did not find any unannotated data in database", - None, - "" - ) - # prepare return values - alert_message = None - vis_com_name = vis_com.name - img_src = f"data:image/png;base64, {vis_com.webencoded_image()}" - logging.info("updated visual communication") - return ( - alert_message, - vis_com_name, - img_src - ) + msg = f"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) -- 2.54.0