Skip to content

Commit 6623cf9

Browse files
committed
Merge commit 'c5bd5808563456204441275c32b2d6a513519a61'
2 parents 0e18e08 + c5bd580 commit 6623cf9

File tree

11 files changed

+113
-98
lines changed

11 files changed

+113
-98
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,6 @@ tags
7474
# static archival files
7575
static/tmp*
7676

77+
# VSCode
78+
.devcontainer
79+
devcontainer.json

Containerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM ghcr.io/clamsproject/clams-python-opencv4:1.0.0
2+
3+
WORKDIR ./app
4+
5+
COPY ./requirements.txt .
6+
7+
RUN pip install -r requirements.txt
8+
9+
COPY ./ ./
10+
11+
CMD ["python", "app.py"]

Dockerfile

Lines changed: 0 additions & 15 deletions
This file was deleted.

README.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
# The MMIF Visualization Server
22

3-
This application creates an HTML server that visualizes annotation components in a [MMIF](https://mmif.clams.ai) file. Supported annotations are:
3+
This application creates an HTML server that visualizes annotation components in a [MMIF](https://mmif.clams.ai) file. It contains the following visualizations for any valid MMIF:
44

5-
- Video or Audio file player with HTML5.
6-
- [WebVTT](https://www.w3.org/TR/webvtt1/) for showing alignments.
5+
- Video or Audio file player with HTML5 (assuming file refers to video and/or audio document).
76
- Pretty-printed MMIF contents.
8-
- Javascript for bounding boxes.
9-
- Named entity annotations with [displaCy.](https://explosion.ai/demos/displacy-ent)
7+
- Interactive, searchable MMIF tree view with [JSTree](https://www.jstree.com/).
8+
- Embedded [Universal Viewer](https://universalviewer.io/) (assuming file refers to video and/or image document).
9+
10+
11+
The application also includes tailored visualizations depending on the annotations present in the input MMIF:
12+
| Visualization | Supported CLAMS apps |
13+
|---|---|
14+
| [WebVTT](https://www.w3.org/TR/webvtt1/) for showing alignments of video captions. | [Whisper](https://github.com/clamsproject/app-whisper-wrapper), [Kaldi](https://github.com/clamsproject/app-aapb-pua-kaldi-wrapper) |
15+
| Javascript bounding boxes for image and OCR annotations. | [Tesseract](https://github.com/clamsproject/app-tesseractocr-wrapper), [EAST](https://github.com/clamsproject/app-east-textdetection) |
16+
| Named entity annotations with [displaCy.](https://explosion.ai/demos/displacy-ent) | [SPACY](https://github.com/clamsproject/app-spacy-wrapper) | |
17+
18+
1019

1120
Requirements:
1221

1322
- A command line interface.
1423
- Git (to get the code).
15-
- [Docker](https://www.docker.com/) (if you run the visualizer using Docker).
16-
- Python 3.6 or later (if you want to run the server without Docker).
24+
- [Docker](https://www.docker.com/) or [Podman](https://podman.io/) (if you run the visualizer in a container).
25+
- Python 3.6 or later (if you want to run the server containerless).
1726

1827
To get this code if you don't already have it:
1928

@@ -23,12 +32,12 @@ $ git clone https://github.com/clamsproject/mmif-visualizer
2332

2433

2534

26-
## Running the server in a Docker container
35+
## Running the server in a container
2736

28-
Download or clone this repository and build an image using the `Dockerfile` (you may use another name for the -t parameter, for this example we use `clams-mmif-visualizer` throughout).
37+
Download or clone this repository and build an image using the `Dockerfile` (you may use another name for the -t parameter, for this example we use `clams-mmif-visualizer` throughout). **NOTE**: if using podman, just substitute `docker` for `podman` in the following commands.
2938

3039
```bash
31-
$ docker build -t clams-mmif-visualizer .
40+
$ docker build . -f Containerfile -t clams-mmif-visualizer
3241
```
3342

3443
In these notes we assume that the data are in a local directory named `/Users/Shared/archive` with sub directories `audio`, `image`, `text` and `video` (those subdirectories are standard in CLAMS, but the parent directory could be any directory depending on your local set up). We can now run a Docker container with
@@ -56,7 +65,7 @@ With this, the mounted directory `/data` in the container is accessable from ins
5665

5766

5867

59-
## Running the server without Docker
68+
## Running the server without Docker/Podman
6069

6170
First install the python dependencies listed in `requirements.txt`:
6271

app.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,11 @@ def index():
1919
def ocrpage():
2020
data = request.form
2121
try:
22-
# print(html.unescape(data['frames_pages']))
2322
frames_pages = eval(html.unescape(data['frames_pages']))
2423
page_number = int(data['page_number'])
2524

2625
return (render_ocr(data['vid_path'], frames_pages, page_number))
2726
except Exception as e:
28-
print(html.unescape(data['frames_pages']))
2927
return f'<p class="error">Unexpected error of type {type(e)}: {e}</h1>'
3028
pass
3129

@@ -35,8 +33,11 @@ def upload():
3533
# unavailable because no secret key was set). This was solved in the
3634
# __main__ block by setting a key.
3735
if request.method == 'POST':
38-
# check if the post request has the file part
39-
if 'file' not in request.files:
36+
# Check if request is coming from elasticsearch
37+
if 'data' in request.form:
38+
return render_mmif(request.form['data'])
39+
# Otherwise, check if the post request has the file part
40+
elif 'file' not in request.files:
4041
flash('WARNING: post request has no file part')
4142
return redirect(request.url)
4243
file = request.files['file']

displacy/__init__.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,19 @@ def entity_dict(mmif, view, document_id, app_root):
3030
displacy_dict['ents'] = []
3131
for ann in view['annotations']:
3232
if ann.at_type == Uri.NE:
33-
displacy_dict['ents'].append(entity(ann))
33+
displacy_dict['ents'].append(entity(view, ann))
3434
return displacy_dict
3535

3636

3737
def get_text_documents(mmif):
3838
"""Return a dictionary indexed on document identifiers (with the view identifier
3939
if needed) with text documents as the values."""
40-
tds = [d for d in mmif.documents if str(d.at_type).endswith('TextDocument')]
40+
tds = [d for d in mmif.documents if "TextDocument" in str(d.at_type)]
4141
tds = {td.id:td for td in tds}
4242
for view in mmif.views:
4343
# TODO: add check for TextDocument in metadata.contains (saves time)
4444
for annotation in view.annotations:
45-
if str(annotation.at_type).endswith('TextDocument'):
45+
if "TextDocument" in str(annotation.at_type):
4646
tds["%s:%s" % (view.id, annotation.id)] = annotation
4747
return tds
4848

@@ -74,7 +74,7 @@ def mmif_to_dict(mmif: Mmif):
7474
# to a TextDocument in the views or a set of TextDocuments in the views.
7575
transcript_location = None
7676
for document in mmif.documents:
77-
if document.at_type.endswith('TextDocument'):
77+
if "TextDocument" in document.at_type:
7878
transcript_location = document.location
7979
transcript_location = transcript_location
8080
displacy_dict = {}
@@ -85,13 +85,19 @@ def mmif_to_dict(mmif: Mmif):
8585
displacy_dict['ents'] = []
8686
for ann in ne_view['annotations']:
8787
if ann.at_type == Uri.NE:
88-
displacy_dict['ents'].append(entity(ann))
88+
displacy_dict['ents'].append(entity(ne_view, ann))
8989
return displacy_dict
9090

9191

92-
def entity(annotation: Annotation):
93-
return {'start': annotation.properties['start'],
94-
'end': annotation.properties['end'],
92+
def entity(view: View, annotation: Annotation):
93+
if "targets" in annotation.properties:
94+
start = min([view.annotations[target].properties["start"] for target in annotation.properties["targets"]])
95+
end = max([view.annotations[target].properties["end"] for target in annotation.properties["targets"]])
96+
else:
97+
start = annotation.properties['start']
98+
end = annotation.properties['end']
99+
return {'start': start,
100+
'end': end,
95101
'label': annotation.properties['category']}
96102

97103

ocr.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from flask import render_template
77

88

9-
def add_bounding_box(anno, frames):
10-
frame_num = anno.properties["frame"]
9+
def add_bounding_box(anno, frames, fps):
10+
frame_num = anno.properties.get("frame") or anno.properties.get("timePoint")
1111
box_id = anno.properties["id"]
1212
boxType = anno.properties["boxType"]
1313
coordinates = anno.properties["coordinates"]
@@ -21,17 +21,18 @@ def add_bounding_box(anno, frames):
2121
frames[frame_num]["bb_ids"].append(box_id)
2222
else:
2323
frames[frame_num] = {"boxes": [box], "text": [], "bb_ids": [box_id], "timestamp": None, "secs": None, "repeat": False}
24+
if fps:
25+
secs = int(frame_num/fps)
26+
frames[frame_num]["timestamp"] = str(datetime.timedelta(seconds=secs))
27+
frames[frame_num]["secs"] = secs
28+
2429
return frames
2530

2631

27-
def align_annotations(frames_list, alignments, text_docs, fps):
32+
def align_annotations(frames_list, alignments, text_docs):
2833
"""Link alignments with frames"""
2934
prev_frame = None
3035
for frame_num, frame in frames_list:
31-
if fps:
32-
secs = int(frame_num/fps)
33-
frame["timestamp"] = str(datetime.timedelta(seconds=secs))
34-
frame["secs"] = secs
3536
for box_id in frame["bb_ids"]:
3637
text_id = alignments[box_id]
3738
frame["text"].append(text_docs[text_id])
@@ -96,16 +97,10 @@ def round_boxes(boxes):
9697
return rounded_boxes
9798

9899
def get_ocr_views(mmif):
99-
"""Return OCR views, which have TextDocument and Alignment annotations, but no
100-
other annotations."""
100+
"""Return OCR views, which have TextDocument, BoundingBox, and Alignment annotations"""
101101
views = []
102-
# TODO: not sure why we use the full URL
103-
needed_types = set([
104-
"http://mmif.clams.ai/0.4.0/vocabulary/TextDocument",
105-
"http://mmif.clams.ai/0.4.0/vocabulary/BoundingBox",
106-
"http://mmif.clams.ai/0.4.0/vocabulary/Alignment" ])
102+
ocr_apps = ["east-textdetection", "tesseract"]
107103
for view in mmif.views:
108-
annotation_types = view.metadata.contains.keys()
109-
if needed_types.issubset(annotation_types) and len(annotation_types) == 3:
104+
if any([view.metadata.app.find(ocr_app) for ocr_app in ocr_apps]):
110105
views.append(view)
111106
return views

requirements.txt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
spacy==2.3.2
2-
lapps==0.0.2
3-
clams-python==0.5.0
4-
opencv-python==4.4.0.44
2+
clams-python==1.0.0

templates/interactive.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
<li data-jstree='{"type":"view"}'>{{view.metadata.app}} ({{view.id}})
7676
<ul>
7777
{% for annotation in view.annotations %}
78-
<li data-jstree='{"type": "{{"annotation-highlighted" if cluster.highlighted and is_aligned else "annotation"}}"}'>{{annotation.at_type}}
78+
<li data-jstree='{"type": "{{"annotation-highlighted" if cluster.highlighted and view.id in aligned_views else "annotation"}}"}'>{{annotation.at_type}}
7979
<ul>
8080
<li data-jstree='{"type":"properties"}'>{{annotation.properties}}</li>
8181
</ul>

templates/ocr.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
<h4>
1414
frame: {{frame_num}}<br>
1515
timestamp: <a class="timestamp" onclick="SetCurTime('{{secs}}')">{{frame["timestamp"]}}</a><br>
16-
text detected:<br>
17-
{% for text in frame["text"] %}
18-
&emsp;{{text}}<br>
19-
{% endfor %}
16+
{% if frame["text"] %}
17+
text detected:<br>
18+
{% for text in frame["text"] %}
19+
&emsp;{{text}}<br>
20+
{% endfor %}
21+
{% endif %}
2022
</h4>
2123
</div>
2224
</div>

0 commit comments

Comments
 (0)