Merge pull request 'add_upload_button' (#21) from add_upload_button into main
Code Quality Pipeline / Test (push) Successful in 56s
pipeline / Show (push) Successful in 5s

Reviewed-on: http://192.168.1.2:3000/brian/visual_critical_discourse_analysis/pulls/21
This commit was merged in pull request #21.
This commit is contained in:
Brian Bjarke Jensen
2024-02-28 20:20:30 +01:00
7 changed files with 201 additions and 71 deletions
+11 -12
View File
@@ -1,13 +1,12 @@
from .classes import ( from __future__ import annotations
ModelOutputs,
VisualCommunication, from .classes import ModelOutputs
NoDocumentFoundException from .classes import NoDocumentFoundException
) from .classes import VisualCommunication
from .database import connect from .database import connect
from .utils import ( from .utils import get_visual_communication
total_documents, from .utils import total_annotated
total_annotated, from .utils import total_documents
get_visual_communication, from .utils import upsert_annotations
upsert_annotations, from .utils import upsert_predictions
upsert_predictions, from .utils import upsert_visual_communication
)
+7
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from base64 import b64decode
from base64 import b64encode from base64 import b64encode
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
@@ -122,6 +123,12 @@ class VisualCommunication(BaseModel):
image.load() image.load()
return VisualCommunication(name=name, image=image) return VisualCommunication(name=name, image=image)
@classmethod
def decode_image(cls, content: str) -> Image.Image:
"""Decode image."""
_, content_data = content.split(',')
return Image.open(BytesIO(b64decode(content_data)))
@field_serializer('image') @field_serializer('image')
def serialize_image(image: Image.Image) -> bytes: # type: ignore def serialize_image(image: Image.Image) -> bytes: # type: ignore
buffer = BytesIO() buffer = BytesIO()
+52 -33
View File
@@ -1,57 +1,58 @@
from pymongo.collection import Collection from __future__ import annotations
import logging import logging
from .classes import ( from pymongo.collection import Collection
VisualCommunication,
NoDocumentFoundException, from .classes import ModelOutputs
ModelOutputs from .classes import NoDocumentFoundException
) from .classes import VisualCommunication
def total_documents( def total_documents(
collection: Collection collection: Collection,
) -> int: ) -> int:
"""Get total number of documents in database.""" """Get total number of documents in database."""
return collection.count_documents(filter={}) return collection.count_documents(filter={})
def total_annotated( def total_annotated(
collection: Collection collection: Collection,
) -> int: ) -> int:
"""Get total number of annotated documents in database.""" """Get total number of annotated documents in database."""
query = { query = {
"annotation": { 'annotation': {
"$ne": None '$ne': None,
} },
} }
return collection.count_documents(filter=query) return collection.count_documents(filter=query)
def get_visual_communication( def get_visual_communication(
collection: Collection, collection: Collection,
with_annotation: bool = False with_annotation: bool = False,
) -> VisualCommunication: ) -> VisualCommunication:
"""Get a random visual communication from the database.""" """Get a random visual communication from the database."""
query = {} query = {}
if with_annotation: if with_annotation:
query["annotation"] = {"$ne": None} query['annotation'] = {'$ne': None}
else: else:
query["annotation"] = {"$eq": None} query['annotation'] = {'$eq': None}
data = collection.aggregate([ data = collection.aggregate([
{ {
"$match": query # find using filters '$match': query, # find using filters
}, },
{ {
"$sample": { '$sample': {
"size": 1 # get one random 'size': 1, # get one random
} },
} },
]) ])
data_list = list(data) # read data from cursor object data_list = list(data) # read data from cursor object
if len(data_list) == 0: if len(data_list) == 0:
logging.error("failed getting visual communication") logging.error('failed getting visual communication')
raise NoDocumentFoundException() raise NoDocumentFoundException()
logging.info("finished") logging.info('finished')
return VisualCommunication.model_validate(data_list[0]) return VisualCommunication.model_validate(data_list[0])
@@ -62,20 +63,20 @@ def upsert_predictions(
) -> None: ) -> None:
"""Upsert prediction data in the database.""" """Upsert prediction data in the database."""
query = { query = {
"name": vis_com_name 'name': vis_com_name,
} }
update = { update = {
"$set": { '$set': {
"prediction": predictions.model_dump() 'prediction': predictions.model_dump(),
} },
} }
res = collection.update_one( res = collection.update_one(
filter=query, filter=query,
update=update, update=update,
upsert=True, upsert=True,
) )
logging.debug("upserted document: %s", res) logging.debug('upserted document: %s', res)
logging.info("finished") logging.info('finished')
def upsert_annotations( def upsert_annotations(
@@ -85,17 +86,35 @@ def upsert_annotations(
) -> None: ) -> None:
"""Upserts annotation data in the database.""" """Upserts annotation data in the database."""
query = { query = {
"name": vis_com_name 'name': vis_com_name,
} }
update = { update = {
"$set": { '$set': {
"annotation": annotations.model_dump() 'annotation': annotations.model_dump(),
} },
} }
res = collection.update_one( res = collection.update_one(
filter=query, filter=query,
update=update, update=update,
upsert=True, upsert=True,
) )
logging.info("upserted document: %s", res) logging.info('upserted document: %s', res)
logging.info("finished") logging.info('finished')
def upsert_visual_communication(
collection: Collection,
visual_communication_list: list[VisualCommunication],
) -> bool:
"""
Upsert VisualCommunication object in the database.
Returns bool stating success.
"""
response = collection.insert_many(
[
vis_com.model_dump()
for vis_com
in visual_communication_list
],
)
return response.acknowledged
+66 -2
View File
@@ -18,6 +18,8 @@ from src.database import get_visual_communication
from src.database import ModelOutputs from src.database import ModelOutputs
from src.database import NoDocumentFoundException from src.database import NoDocumentFoundException
from src.database import upsert_annotations from src.database import upsert_annotations
from src.database import upsert_visual_communication
from src.database import VisualCommunication
# setup app # setup app
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
@@ -46,12 +48,74 @@ def show_alert(
): ):
if msg is None or msg == '': if msg is None or msg == '':
return False, '' return False, ''
logging.info(f"updated alert message: {msg}") logging.info(f'updated alert message: {msg}')
return True, msg return True, msg
@app.callback( @app.callback(
Output('alert-message', 'data'), 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('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('vis-com-name', 'data'),
Output('image-container', 'src'), Output('image-container', 'src'),
Output({'type': 'annotation', 'index': ALL}, 'value'), Output({'type': 'annotation', 'index': ALL}, 'value'),
+13 -5
View File
@@ -1,15 +1,23 @@
from dash import html from __future__ import annotations
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
from dash import html
alerts_element = html.Div( alerts_element = html.Div(
children=[ children=[
dbc.Alert( dbc.Alert(
children="", children='',
id="alert-element", id='alert-element',
dismissable=True, dismissable=True,
fade=False, fade=False,
is_open=False, is_open=False,
) ),
] dbc.Alert(
children='',
id='success-element',
is_open=False,
duration=1000,
),
],
) )
+36 -8
View File
@@ -1,21 +1,49 @@
from __future__ import annotations
import dash_mantine_components as dmc import dash_mantine_components as dmc
from dash import dcc
from dash import html from dash import html
title_element = dmc.Center(
children=[
html.H2(
children=[
'Visual Critical Discourse Analysis Tool',
],
),
],
)
upload_button_element = dcc.Upload(
children=[
dmc.Button(
'upload images'.title(),
id='upload-button',
color='lime',
n_clicks=0,
variant='outline',
radius='sm',
size='md',
),
],
multiple=True,
id='upload-data',
accept='image/png,image/jpeg,image/jpg',
)
header_element = dmc.Header( header_element = dmc.Header(
height=80, height=80,
children=[ children=[
dmc.Container( dmc.Group(
children=[ children=[
html.H2( title_element,
children="Visual Critical Discourse Analysis Tool", upload_button_element,
],
position='apart',
style={ style={
"margin-left": "20px", 'margin': '10px',
"padding-top": "-5px" 'padding': '10px',
}, },
), ),
], ],
size="xl", px="xl",
),
]
) )
+14 -9
View File
@@ -1,18 +1,23 @@
from dash import html, dcc from __future__ import annotations
import logging import logging
import os import os
storage_type = "session" from dash import dcc
if "ENV" in os.environ and os.getenv("ENV") == "DEV": from dash import html
storage_type = "memory"
storage_type = 'session'
if 'ENV' in os.environ and os.getenv('ENV') == 'DEV':
storage_type = 'memory'
logging.info( logging.info(
"ENV=DEV -> dcc.Stores changed to storage_type=%s", 'ENV=DEV -> dcc.Stores changed to storage_type=%s',
storage_type storage_type,
) )
stores_element = html.Div( stores_element = html.Div(
children=[ children=[
dcc.Store(id="alert-message", storage_type=storage_type, data=""), dcc.Store(id='alert-message', storage_type=storage_type, data=''),
dcc.Store(id="vis-com-name", storage_type=storage_type, data=""), dcc.Store(id='success-message', storage_type=storage_type, data=''),
] dcc.Store(id='vis-com-name', storage_type=storage_type, data=''),
],
) )