Compare commits
5 commits
9c54b2f8d7
...
adce8d067c
Author | SHA1 | Date | |
---|---|---|---|
|
adce8d067c | ||
|
4b0b5c2c16 | ||
|
5fa5096c44 | ||
|
ab9c66794c | ||
|
232903c0ff |
9 changed files with 969 additions and 165 deletions
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
import asyncio
|
||||
import copy
|
||||
from ctypes.wintypes import tagMSG
|
||||
import json
|
||||
from os import X_OK, PathLike
|
||||
import os
|
||||
|
@ -12,6 +13,9 @@ import svgwrite
|
|||
import tempfile
|
||||
import io
|
||||
import logging
|
||||
from anytree import NodeMixin, RenderTree, iterators
|
||||
from anytree.exporter import JsonExporter, DictExporter
|
||||
from anytree.importer import JsonImporter, DictImporter
|
||||
|
||||
logger = logging.getLogger('svganim.strokes')
|
||||
|
||||
|
@ -19,11 +23,12 @@ Milliseconds = float
|
|||
Seconds = float
|
||||
|
||||
class Annotation:
|
||||
def __init__(self, tag: str, drawing: Drawing, t_in: Milliseconds, t_out: Milliseconds) -> None:
|
||||
def __init__(self, tag: str, drawing: Drawing, t_in: Milliseconds, t_out: Milliseconds, comment: str = None) -> None:
|
||||
self.tag = tag
|
||||
self.t_in = t_in
|
||||
self.t_out = t_out
|
||||
self.drawing = drawing
|
||||
self.comment = comment
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
|
@ -232,7 +237,7 @@ class AnimationSlice:
|
|||
strokes = self.getStrokeSlices(frame_in, frame_out, t_in)
|
||||
# TODO shift t of points with t_in
|
||||
viewboxes = self.getViewboxesSlice(t_in, t_out)
|
||||
|
||||
|
||||
audio = self.audio.getSlice(t_in, t_out) if self.audio else None
|
||||
return AnimationSlice([self.id[0], t_in, t_out], strokes, viewboxes, t_in, t_out, audio)
|
||||
|
||||
|
@ -499,6 +504,8 @@ class AnnotationIndex:
|
|||
self.drawing_dir = drawing_dir
|
||||
self.metadata_dir = metadata_dir
|
||||
|
||||
self.root_tag = getRootTag()
|
||||
|
||||
# disable disk cache because of glitches shelve.open(filename, writeback=True)
|
||||
self.shelve = {}
|
||||
|
||||
|
@ -515,7 +522,9 @@ class AnnotationIndex:
|
|||
Drawing(fn, self.metadata_dir, self.drawing_dir) for fn in self.get_drawing_filenames()
|
||||
]
|
||||
}
|
||||
self.shelve['_tags'] = {}
|
||||
|
||||
self.root_tag = getRootTag()
|
||||
self.shelve['_tags'] = {tag.id: [] for tag in self.root_tag.descendants}
|
||||
self.shelve['_annotations'] = {}
|
||||
|
||||
drawing: Drawing
|
||||
|
@ -525,14 +534,19 @@ class AnnotationIndex:
|
|||
continue
|
||||
for ann in meta['annotations']:
|
||||
annotation = Annotation(
|
||||
ann['tag'], drawing, ann['t_in'], ann['t_out'])
|
||||
ann['tag'], drawing, ann['t_in'], ann['t_out'], ann['comment'] if 'comment' in ann else "")
|
||||
self.shelve['_annotations'][annotation.id] = annotation
|
||||
if annotation.tag not in self.shelve['_tags']:
|
||||
self.shelve['_tags'][annotation.tag] = [annotation]
|
||||
logger.error(f"Use of non-existing tag {annotation.tag}")
|
||||
else:
|
||||
self.shelve['_tags'][annotation.tag].append(
|
||||
annotation
|
||||
)
|
||||
tag = self.root_tag.find_by_id(annotation.tag)
|
||||
if tag is not None:
|
||||
tag.annotation_count += 1
|
||||
|
||||
|
||||
@property
|
||||
def drawings(self) -> dict[str, Drawing]:
|
||||
|
@ -545,11 +559,14 @@ class AnnotationIndex:
|
|||
@property
|
||||
def annotations(self) -> dict[str, Annotation]:
|
||||
return self.shelve["_annotations"]
|
||||
|
||||
def has_tag(self, tag):
|
||||
return tag in self.tags
|
||||
|
||||
def get_annotations(self, tag) -> list[Annotation]:
|
||||
if tag not in self.tags:
|
||||
def get_annotations_for_tag(self, tag_id) -> list[Annotation]:
|
||||
if tag_id not in self.tags:
|
||||
return []
|
||||
return self.tags[tag]
|
||||
return self.tags[tag_id]
|
||||
|
||||
def get_drawing_names(self) -> list[str]:
|
||||
return [
|
||||
|
@ -563,6 +580,14 @@ class AnnotationIndex:
|
|||
os.path.join(self.drawing_dir, f"{name}.json_appendable")
|
||||
for name in self.get_drawing_names()
|
||||
]
|
||||
|
||||
def get_nested_annotations_for_tag(self, tag_id) -> list[Annotation]:
|
||||
tag = self.root_tag.find_by_id(tag_id)
|
||||
annotations = []
|
||||
for tag in tag.descendants_incl_self():
|
||||
annotations.extend(self.get_annotations_for_tag(tag.id))
|
||||
return annotations
|
||||
|
||||
|
||||
def __del__(self):
|
||||
self.shelve.close()
|
||||
|
@ -693,3 +718,56 @@ def strokes2D(strokes):
|
|||
d += f"{rel_stroke[0]},{rel_stroke[1]} "
|
||||
last_stroke = stroke
|
||||
return d
|
||||
|
||||
class Tag(NodeMixin):
|
||||
def __init__(self, id, name = None, description = "", color = None, parent=None, children=None, annotation_count=None):
|
||||
self.id = id
|
||||
self.name = self.id if name is None else name
|
||||
self.color = color
|
||||
self.description = description
|
||||
self.parent = parent
|
||||
if children:
|
||||
self.children = children
|
||||
|
||||
self.annotation_count = 0 #always zero!
|
||||
|
||||
if self.id == 'root' and not self.is_root:
|
||||
logger.error("Root node shouldn't have a parent assigned")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<svganim.strokes.Tag {self.id}>"
|
||||
|
||||
def __str__(self):
|
||||
return RenderTree(self).by_attr('name')
|
||||
|
||||
def get_color(self):
|
||||
if self.color is None and self.parent is not None:
|
||||
return self.parent.get_color()
|
||||
return self.color
|
||||
|
||||
def descendants_incl_self(self):
|
||||
return tuple(iterators.PreOrderIter(self))
|
||||
|
||||
def find_by_id(self, tag_id) -> Optional[Tag]:
|
||||
for t in self.descendants:
|
||||
if t.id == tag_id:
|
||||
return t
|
||||
return None
|
||||
|
||||
def toJson(self, with_counts=False) -> str:
|
||||
ignore_counts=lambda attrs: [(k, v) for k, v in attrs if k != "annotation_count"]
|
||||
attrFilter = None if with_counts else ignore_counts
|
||||
|
||||
return JsonExporter(DictExporter(attriter=attrFilter), indent=2).export(self)
|
||||
|
||||
def loadTagFromJson(string) -> Tag:
|
||||
tree: Tag = JsonImporter(DictImporter(Tag)).import_(string)
|
||||
return tree
|
||||
|
||||
def getRootTag(file = 'www/tags.json') -> Tag:
|
||||
with open(file, 'r') as fp:
|
||||
tree: Tag = JsonImporter(DictImporter(Tag)).read(fp)
|
||||
|
||||
return tree
|
||||
|
||||
# print(RenderTree(tree))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from hashlib import md5
|
||||
|
||||
|
||||
def annotation_hash(handler, input):
|
||||
def annotation_hash(handler=None, input =""):
|
||||
return md5(input.encode()).hexdigest()
|
||||
|
||||
# def nmbr(handler, lst) -> int:
|
||||
|
|
|
@ -10,22 +10,112 @@
|
|||
color: white
|
||||
}
|
||||
|
||||
ul {
|
||||
#tags {
|
||||
font-size: 80%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#tags li {
|
||||
line-height: 1.5;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#tags .tag-id-root {
|
||||
list-style: none;
|
||||
;
|
||||
}
|
||||
|
||||
#tags .tag-id-root>ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
#tags li>div,
|
||||
#tags li.add-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tags .tag-id-root>div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* #tags li:hover>ul>li.add-tag {
|
||||
visibility: visible;
|
||||
;
|
||||
} */
|
||||
#tags li.selected>ul>li.add-tag {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#tags>li>ul>li.add-tag {
|
||||
display: block;
|
||||
;
|
||||
}
|
||||
|
||||
summary h2{
|
||||
#annotations .annotations-actions {
|
||||
margin-bottom: 10px;
|
||||
color: darkgray;
|
||||
font-size: 80%;
|
||||
border-bottom: solid 3px #444;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#tags .add-tag {
|
||||
display: none;
|
||||
;
|
||||
color: lightgray;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
#tags input[type="color"] {
|
||||
cursor: pointer;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
padding: 0;
|
||||
border: solid 1px black;
|
||||
border-radius: 2px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#tags li:hover>div>input.rm-tag {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#tags li div {
|
||||
position: relative
|
||||
}
|
||||
|
||||
#tags input.rm-tag:hover {
|
||||
color: red;
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
#tags input.rm-tag {
|
||||
/* display: none; */
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tags .selected>div {
|
||||
background: lightblue
|
||||
}
|
||||
|
||||
|
||||
summary h2 {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details[open] summary{
|
||||
details[open] summary {
|
||||
color: rgb(224, 196, 196);
|
||||
}
|
||||
|
||||
|
@ -37,101 +127,84 @@
|
|||
display: block;;
|
||||
} */
|
||||
|
||||
img {
|
||||
|
||||
|
||||
#annotation_manager {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: 200px auto;
|
||||
}
|
||||
|
||||
#tags {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
#annotations {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
#annotations ul {
|
||||
grid-template-columns: repeat(auto-fill, 320px);
|
||||
grid-gap: 20px;
|
||||
display: grid;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
#annotations li {}
|
||||
|
||||
#annotations img {
|
||||
/* width: 400px; */
|
||||
background: white;
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
cursor: pointer;
|
||||
padding: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.svganim_player {
|
||||
#annotations .svganim_player {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
padding: 10px;
|
||||
|
||||
background: white;
|
||||
}
|
||||
|
||||
.svganim_player svg {
|
||||
#annotations .svganim_player svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.svganim_player.play:not(.loading) .controls {
|
||||
#annotations .svganim_player.play:not(.loading) .controls {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.svganim_player:hover .controls {
|
||||
#annotations .svganim_player:hover .controls {
|
||||
visibility: visible !important;
|
||||
}
|
||||
</style>
|
||||
<script src="assets/nouislider-15.5.0.js"></script>
|
||||
<script src="assets/wNumb-1.2.0.min.js"></script>
|
||||
<script src="annotate.js"></script>
|
||||
<script src="playlist.js"></script>
|
||||
<script src="annotations.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% for tag in index.tags %}
|
||||
<details>
|
||||
<summary>
|
||||
<h2>{{tag}} ({{len(index.tags[tag])}})</h2>
|
||||
</summary>
|
||||
<ul>
|
||||
{% for annotation in index.tags[tag] %}
|
||||
<li>
|
||||
<img src="/annotation/{{ annotation.id }}.svg" loading="lazy" id="img-{{ annotation_hash(annotation.id) }}">
|
||||
<div class="play" id="annotation-{{ annotation_hash(annotation.id) }}"></div>
|
||||
<script type='text/javascript'>
|
||||
(function () {
|
||||
let imgEl = document.getElementById('img-{{ annotation_hash(annotation.id) }}');
|
||||
imgEl.addEventListener('click', () => {
|
||||
imgEl.style.display = 'none';
|
||||
new Annotator(
|
||||
document.getElementById("annotation-{{ annotation_hash(annotation.id) }}"),
|
||||
"tags.json",
|
||||
"{{ annotation.getJsonUrl() }}",
|
||||
{ is_player: true, crop_to_fit: true, autoplay: true }
|
||||
);
|
||||
})
|
||||
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</li>
|
||||
<!-- <li><img src="/annotation/{{ annotation.id }}.svg" data-audio="/annotation/{{ annotation.id }}.mp3"></li> -->
|
||||
{% end %}
|
||||
</ul>
|
||||
</details>
|
||||
{% end %}
|
||||
<!-- <ul>
|
||||
{% for annotation in index.annotations %}
|
||||
<li>{{ annotation }}</li>
|
||||
{% end %}
|
||||
</ul> -->
|
||||
<div id="annotation_manager">
|
||||
<ul id="tags"></ul>
|
||||
<div id="annotations"></div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<a href="?refresh=1">Reload index</a>
|
||||
</body>
|
||||
<script>
|
||||
let images = document.querySelectorAll('[data-audio]');
|
||||
for (const image of images) {
|
||||
const audio = new Audio(image.dataset.audio);
|
||||
console.log(image, audio);
|
||||
image.addEventListener('mouseover', (e) => {
|
||||
audio.play();
|
||||
});
|
||||
image.addEventListener('mouseout', (e) => {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
});
|
||||
}
|
||||
const am = new AnnotationManager(document.getElementById('annotation_manager'), 'tags.json');
|
||||
</script>
|
||||
|
||||
</html>
|
|
@ -1,6 +1,8 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from urllib.error import HTTPError
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
|
@ -11,6 +13,7 @@ import html
|
|||
import argparse
|
||||
import coloredlogs
|
||||
import glob
|
||||
import filelock
|
||||
import svganim.strokes
|
||||
import svganim.uimethods
|
||||
|
||||
|
@ -345,12 +348,21 @@ class TagAnnotationsHandler(tornado.web.RequestHandler):
|
|||
self.metadir = os.path.join(self.config.storage, "metadata")
|
||||
|
||||
def get(self, tag):
|
||||
if tag not in self.index.tags:
|
||||
if not self.index.has_tag(tag):
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
self.set_header("Content-Type", "application/json")
|
||||
annotations = self.index.tags[tag]
|
||||
self.write(json.dumps(list([a.id for a in annotations])))
|
||||
# annotations = self.index.tags[tag]
|
||||
# self.write(json.dumps(list([a.id for a in annotations])))
|
||||
annotations = self.index.get_nested_annotations_for_tag(tag)
|
||||
self.write(json.dumps([{
|
||||
"id": annotation.id,
|
||||
"tag": annotation.tag,
|
||||
"id_hash": svganim.uimethods.annotation_hash(input=annotation.id),
|
||||
"url": annotation.getJsonUrl(),
|
||||
"comment": annotation.comment,
|
||||
"drawing": annotation.drawing.get_url()
|
||||
} for annotation in annotations]))
|
||||
|
||||
|
||||
class AnnotationHandler(tornado.web.RequestHandler):
|
||||
|
@ -400,6 +412,41 @@ class AnnotationHandler(tornado.web.RequestHandler):
|
|||
"tag": annotation.tag,
|
||||
"audio": f"/annotation/{annotation.id}.mp3",
|
||||
}))
|
||||
|
||||
def post(self, annotation_id):
|
||||
"""change tag for given annotation"""
|
||||
if annotation_id not in self.index.annotations:
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
# might be set on file level, but let's try to avoid issues by keeping it simple
|
||||
lock = filelock.FileLock("metadata_write.lock", timeout=10)
|
||||
with lock:
|
||||
newTagId = self.get_argument('tag_id')
|
||||
if not self.index.has_tag(newTagId):
|
||||
raise tornado.web.HTTPError(400)
|
||||
|
||||
annotation: svganim.strokes.Annotation = self.index.annotations[annotation_id]
|
||||
|
||||
logger.info(f"change tag from {annotation.tag} to {newTagId}")
|
||||
|
||||
# change metadata and reload index
|
||||
metadata = annotation.drawing.get_metadata()
|
||||
change = False
|
||||
for idx, ann in enumerate(metadata['annotations']):
|
||||
if ann['t_in'] == annotation.t_in and ann['t_out'] == annotation.t_out and annotation.tag == ann['tag']:
|
||||
#found!?
|
||||
metadata['annotations'][idx]['tag'] = newTagId
|
||||
change = True
|
||||
break
|
||||
|
||||
if change == False:
|
||||
raise HTTPError(409)
|
||||
|
||||
with open(annotation.drawing.metadata_fn, "w") as fp:
|
||||
logger.info(f"save tag in {annotation.drawing.metadata_fn}")
|
||||
json.dump(metadata, fp)
|
||||
|
||||
self.index.refresh()
|
||||
|
||||
|
||||
class DrawingHandler(tornado.web.RequestHandler):
|
||||
|
@ -504,6 +551,44 @@ class AnnotationsHandler(tornado.web.RequestHandler):
|
|||
with open(meta_file, "w") as fp:
|
||||
json.dump(self.json_args, fp)
|
||||
|
||||
class TagsHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, config, index: svganim.strokes.AnnotationIndex) -> None:
|
||||
self.config = config
|
||||
self.index = index
|
||||
|
||||
def get(self):
|
||||
self.set_header("Content-Type", "application/json")
|
||||
self.write(self.index.root_tag.toJson(with_counts=True))
|
||||
# with open('www/tags.json', 'r') as fp:
|
||||
# # TODO: enrich with counts
|
||||
# self.write(fp.read())
|
||||
|
||||
def put(self):
|
||||
# data = json.loads(self.request.body)
|
||||
tree = svganim.strokes.loadTagFromJson(self.request.body)
|
||||
logger.info(f"New tag tree:\n{tree}")
|
||||
newTagsContent = tree.toJson()
|
||||
# save at minute resolution
|
||||
now = datetime.datetime.utcnow().isoformat(timespec='minutes')
|
||||
|
||||
backup_dir = os.path.join(self.config.storage, 'tag_versions')
|
||||
if not os.path.exists(backup_dir):
|
||||
logger.warning(f"Creating tags backupdir {backup_dir}")
|
||||
os.mkdir(backup_dir)
|
||||
|
||||
bakfile = os.path.join(backup_dir, f'tags.{now}.json')
|
||||
logger.info(f"Creating tags backup {bakfile}" )
|
||||
shutil.copyfile('www/tags.json', bakfile)
|
||||
|
||||
with open('www/tags.json', 'w') as fp:
|
||||
fp.write(newTagsContent)
|
||||
|
||||
# update as to load new tag into cache
|
||||
self.index.refresh()
|
||||
|
||||
self.set_status(204)
|
||||
# print()
|
||||
|
||||
|
||||
class IndexHandler(tornado.web.RequestHandler):
|
||||
"""Get annotation as svg"""
|
||||
|
@ -587,6 +672,9 @@ class Server:
|
|||
(r"/index", IndexHandler,
|
||||
{"config": self.config, "index": self.index}),
|
||||
|
||||
(r"/tags.json", TagsHandler,
|
||||
{"config": self.config, "index": self.index}),
|
||||
|
||||
(r"/(.*)", StaticFileWithHeaderHandler,
|
||||
{"path": self.web_root, 'default_filename': 'index.html'}),
|
||||
],
|
||||
|
|
|
@ -230,30 +230,30 @@ class Annotator extends EventTarget {
|
|||
this.tagsEl = document.createElement('ul');
|
||||
this.tagsEl.classList.add('tags');
|
||||
const addTags = (tags, tagsEl) => {
|
||||
Object.entries(tags).forEach(([tag, tagData]) => {
|
||||
tags.forEach((tag) => {
|
||||
let tagLiEl = document.createElement('li');
|
||||
let tagEl = document.createElement('div');
|
||||
|
||||
tagEl.classList.add('tag');
|
||||
tagEl.dataset.tag = tag;
|
||||
tagEl.innerText = tagData.hasOwnProperty('fullname') ? tagData.fullname : tag;
|
||||
tagEl.dataset.tag = tag.id;
|
||||
tagEl.innerText = tag.hasOwnProperty('name') ? tag.name : tag.id;
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
this.addTag(tag, this.inPointPosition, this.outPointPosition);
|
||||
this.addTag(tag.id, this.inPointPosition, this.outPointPosition);
|
||||
});
|
||||
|
||||
tagEl.title = tagData.hasOwnProperty('description') ? tagData.description : "";
|
||||
tagEl.title = tag.hasOwnProperty('description') ? tag.description : "";
|
||||
|
||||
let signEl = document.createElement('span');
|
||||
signEl.classList.add('annotation-' + tag);
|
||||
signEl.style.backgroundColor = this.getColorForTag(tag);
|
||||
signEl.classList.add('annotation-' + tag.id);
|
||||
signEl.style.backgroundColor = this.getColorForTag(tag.id);
|
||||
tagEl.prepend(signEl);
|
||||
|
||||
tagLiEl.appendChild(tagEl);
|
||||
|
||||
if (tagData.hasOwnProperty('sub')) {
|
||||
if (tag.hasOwnProperty('children')) {
|
||||
const subEl = document.createElement('ul');
|
||||
subEl.classList.add('subtags');
|
||||
addTags(tagData.sub, subEl);
|
||||
addTags(tag.children, subEl);
|
||||
tagLiEl.appendChild(subEl);
|
||||
}
|
||||
|
||||
|
@ -304,14 +304,14 @@ class Annotator extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
getColorForTag(tag) {
|
||||
const tagData = this.tagMap[tag];
|
||||
console.log(tag, tagData);
|
||||
if (tagData && tagData.hasOwnProperty('color')) {
|
||||
return tagData.color;
|
||||
getColorForTag(tag_id) {
|
||||
const tag = this.tagMap[tag_id];
|
||||
console.log(tag_id, tag);
|
||||
if (tag && tag.hasOwnProperty('color')) {
|
||||
return tag.color;
|
||||
}
|
||||
if (tagData && tagData.hasOwnProperty('parent')) {
|
||||
return this.getColorForTag(tagData['parent']);
|
||||
if (tag && tag.hasOwnProperty('parent')) {
|
||||
return this.getColorForTag(tag['parent'].id);
|
||||
}
|
||||
return 'black';
|
||||
}
|
||||
|
@ -794,15 +794,15 @@ class Annotator extends EventTarget {
|
|||
const request = new Request(tagFile);
|
||||
return fetch(request)
|
||||
.then(response => response.json())
|
||||
.then(tags => {
|
||||
this.tags = tags;
|
||||
.then(rootTag => {
|
||||
this.tags = rootTag.children;
|
||||
this.tagMap = {};
|
||||
const addTagsToMap = (tags, parent) => {
|
||||
Object.entries(tags).forEach(([tag, tagData]) => {
|
||||
tagData['parent'] = typeof parent != "undefined" ? parent : null;
|
||||
this.tagMap[tag] = tagData;
|
||||
if (tagData.hasOwnProperty("sub")) {
|
||||
addTagsToMap(tagData.sub, tag);
|
||||
tags.forEach((tag) => {
|
||||
tag['parent'] = typeof parent != "undefined" ? parent : null;
|
||||
this.tagMap[tag.id] = tag;
|
||||
if (tag.hasOwnProperty("children")) {
|
||||
addTagsToMap(tag.children, tag);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
425
app/www/annotations.js
Normal file
425
app/www/annotations.js
Normal file
|
@ -0,0 +1,425 @@
|
|||
class AnnotationManager {
|
||||
constructor(rootEl, tagsUrl) {
|
||||
this.rootEl = rootEl;
|
||||
|
||||
|
||||
this.tagsEl = this.rootEl.querySelector('#tags');
|
||||
this.annotationsEl = this.rootEl.querySelector('#annotations');
|
||||
|
||||
this.selectedAnnotations = [];
|
||||
this.selectedTag = null;
|
||||
this.tagsUrl = tagsUrl;
|
||||
this.loadTags();
|
||||
}
|
||||
|
||||
loadTags() {
|
||||
// tags config
|
||||
const request = new Request(this.tagsUrl);
|
||||
return fetch(request)
|
||||
.then(response => response.json())
|
||||
.then(rootTag => {
|
||||
this.rootTag = Tag.from(rootTag);
|
||||
this.buildTagList()
|
||||
});
|
||||
}
|
||||
|
||||
buildTagList() {
|
||||
// build, and rebuild
|
||||
this.tagsEl.innerHTML = "";
|
||||
|
||||
const addTag = (tag, parentEl) => {
|
||||
const tagLiEl = document.createElement('li');
|
||||
const tagSubEl = document.createElement('ul');
|
||||
const tagEl = document.createElement('div');
|
||||
|
||||
tagEl.innerText = `${tag.get_name()} (${tag.annotation_count ?? 0})`;
|
||||
tagEl.addEventListener('click', (ev) => {
|
||||
this.selectTag(tag);
|
||||
})
|
||||
tagEl.addEventListener('dblclick', (ev) => {
|
||||
this.renameTag(tag);
|
||||
})
|
||||
|
||||
const colorEl = document.createElement('input');
|
||||
colorEl.type = 'color';
|
||||
colorEl.value = tag.get_color();
|
||||
colorEl.addEventListener('change', (ev) => {
|
||||
this.setTagColor(tag, ev.target.value);
|
||||
});
|
||||
tagEl.prepend(colorEl)
|
||||
|
||||
const rmEl = document.createElement('input');
|
||||
rmEl.type = 'button';
|
||||
rmEl.classList.add('rm-tag');
|
||||
rmEl.value = '🗑';
|
||||
rmEl.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation();
|
||||
this.removeTag(tag);
|
||||
});
|
||||
tagEl.appendChild(rmEl)
|
||||
|
||||
tagLiEl.classList.add('tag-id-' + tag.id);
|
||||
tagLiEl.appendChild(tagEl);
|
||||
|
||||
tag.menuLiEl = tagLiEl;
|
||||
|
||||
tagLiEl.appendChild(tagSubEl);
|
||||
tag.children.forEach((tag) => addTag(tag, tagSubEl));
|
||||
const tagAddSubEl = document.createElement('li');
|
||||
tagAddSubEl.classList.add('add-tag')
|
||||
tagAddSubEl.innerText = 'add tag';
|
||||
tagAddSubEl.addEventListener('click', (ev) => {
|
||||
const name = prompt(`Add a tag under '${tag.get_name()}':`);
|
||||
if (name === null || name.length < 1) return //cancel
|
||||
this.addTag(name, tag);
|
||||
});
|
||||
tagSubEl.appendChild(tagAddSubEl);
|
||||
|
||||
|
||||
parentEl.appendChild(tagLiEl);
|
||||
|
||||
};
|
||||
|
||||
addTag(this.rootTag, this.tagsEl);
|
||||
}
|
||||
|
||||
|
||||
selectTag(tag) {
|
||||
this.clearSelectedTag();
|
||||
this.selectedTag = tag;
|
||||
tag.menuLiEl.classList.add('selected');
|
||||
this.loadAnnotationsForTag(tag)
|
||||
}
|
||||
|
||||
clearSelectedTag() {
|
||||
this.selectedTag = null;
|
||||
const selected = this.tagsEl.querySelectorAll('.selected');
|
||||
selected.forEach((s) => s.classList.remove('selected'));
|
||||
this.resetSelectedAnnotations()
|
||||
//TODO empty annotationEl
|
||||
this.annotationsEl.innerHTML = "";
|
||||
}
|
||||
|
||||
selectAnnotation() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
addTag(name, parentTag) {
|
||||
const tag = new Tag();
|
||||
tag.id = crypto.randomUUID();
|
||||
tag.name = name;
|
||||
tag.parent = parentTag;
|
||||
parentTag.children.push(tag);
|
||||
|
||||
this.buildTagList();
|
||||
this.saveTags();
|
||||
}
|
||||
|
||||
loadAnnotationsForTag(tag) {
|
||||
this.annotationsEl.innerHTML = "";
|
||||
|
||||
const request = new Request("/tags/" + tag.id);
|
||||
return fetch(request)
|
||||
.then(response => response.json())
|
||||
.then(annotations => {
|
||||
this.annotations = annotations;
|
||||
this.buildAnnotationList()
|
||||
});
|
||||
}
|
||||
|
||||
buildAnnotationList() {
|
||||
this.annotationsEl.innerHTML = "";
|
||||
|
||||
this.actionsEl = document.createElement('div');
|
||||
this.actionsEl.classList.add('annotations-actions')
|
||||
|
||||
this.buildAnnotationActions();
|
||||
|
||||
this.annotationsEl.appendChild(this.actionsEl);
|
||||
|
||||
|
||||
const ulEl = document.createElement('ul');
|
||||
this.annotations.forEach((annotation, idx) => {
|
||||
const liEl = document.createElement('li');
|
||||
const imgEl = document.createElement('img');
|
||||
const playerEl = document.createElement('div');
|
||||
const infoEl = document.createElement('span');
|
||||
infoEl.classList.add('annotation-info');
|
||||
|
||||
const selectEl = document.createElement('input');
|
||||
selectEl.type = 'checkbox';
|
||||
selectEl.id = 'select-'+annotation.id_hash;
|
||||
selectEl.addEventListener('change', (ev) => {
|
||||
if (ev.target.checked) {
|
||||
this.addAnnotationToSelection(annotation);
|
||||
} else {
|
||||
this.removeAnnotationFromSelection(annotation);
|
||||
}
|
||||
})
|
||||
|
||||
const tag = this.rootTag.find_by_id(annotation.tag);
|
||||
console.log(tag)
|
||||
|
||||
infoEl.innerText = `[${tag.get_name()}] ${annotation.comment}`;
|
||||
|
||||
imgEl.src = `/annotation/${annotation.id}.svg`;
|
||||
imgEl.addEventListener('click', () => {
|
||||
imgEl.style.display = 'none';
|
||||
new Annotator(
|
||||
playerEl,
|
||||
"tags.json",
|
||||
annotation.url,
|
||||
{ is_player: true, crop_to_fit: true, autoplay: true }
|
||||
);
|
||||
})
|
||||
|
||||
playerEl.classList.add('play');
|
||||
|
||||
liEl.appendChild(imgEl);
|
||||
liEl.appendChild(playerEl);
|
||||
liEl.appendChild(selectEl);
|
||||
liEl.appendChild(infoEl);
|
||||
ulEl.appendChild(liEl);
|
||||
|
||||
});
|
||||
|
||||
this.annotationsEl.appendChild(ulEl);
|
||||
|
||||
}
|
||||
|
||||
resetSelectedAnnotations() {
|
||||
this.selectedAnnotations = [];
|
||||
|
||||
this.annotationsEl.querySelectorAll("li input[type='checkbox']").forEach((box) => box.checked = false);
|
||||
this.buildAnnotationActions()
|
||||
}
|
||||
|
||||
addAnnotationToSelection(annotation) {
|
||||
if (this.selectedAnnotations.indexOf(annotation) === -1){
|
||||
this.selectedAnnotations.push(annotation);
|
||||
}
|
||||
this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = true;
|
||||
this.buildAnnotationActions()
|
||||
}
|
||||
removeAnnotationFromSelection(annotation) {
|
||||
if (this.selectedAnnotations.indexOf(annotation) !== -1){
|
||||
this.selectedAnnotations.splice(this.selectedAnnotations.indexOf(annotation), 1)
|
||||
}
|
||||
this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = true;
|
||||
this.buildAnnotationActions()
|
||||
|
||||
}
|
||||
|
||||
buildAnnotationActions() {
|
||||
if(!this.actionsEl || !this.annotations.length) return
|
||||
this.actionsEl.innerHTML = "";
|
||||
|
||||
const selectAllLabelEl = document.createElement('label');
|
||||
selectAllLabelEl.innerText = `Select all`
|
||||
const selectAllEl = document.createElement('input');
|
||||
selectAllEl.type = 'checkbox';
|
||||
selectAllEl.checked = this.annotations.length === this.selectedAnnotations.length;
|
||||
// selectAllEl.innerText = `Select all ${this.annotations.length} items`;
|
||||
selectAllEl.addEventListener('change', (ev) => {
|
||||
if(ev.target.checked) {
|
||||
this.annotations.forEach((a) => this.addAnnotationToSelection(a));
|
||||
} else {
|
||||
this.resetSelectedAnnotations();
|
||||
}
|
||||
});
|
||||
|
||||
selectAllLabelEl.appendChild(selectAllEl)
|
||||
|
||||
this.actionsEl.appendChild(selectAllLabelEl)
|
||||
|
||||
if(!this.selectedAnnotations.length) return;
|
||||
|
||||
const moveLabelEl = document.createElement('label');
|
||||
moveLabelEl.innerText = `Change tag for ${this.selectedAnnotations.length} items`
|
||||
const moveSelectEl = document.createElement('select');
|
||||
this.rootTag.descendants().forEach((tag, i) => {
|
||||
const tagEl = document.createElement('option');
|
||||
tagEl.value = tag.id;
|
||||
if(tag.id == this.selectedTag.id){
|
||||
tagEl.selected = true;
|
||||
}
|
||||
tagEl.innerHTML = tag.get_indented_name();
|
||||
moveSelectEl.appendChild(tagEl);
|
||||
});
|
||||
moveSelectEl.addEventListener('change', (ev) => {
|
||||
const tag = this.rootTag.find_by_id(ev.target.value);
|
||||
console.log(tag);
|
||||
this.moveSelectedAnnotations(tag);
|
||||
})
|
||||
moveLabelEl.appendChild(moveSelectEl)
|
||||
|
||||
|
||||
this.actionsEl.appendChild(moveLabelEl)
|
||||
}
|
||||
|
||||
renameTag(tag) {
|
||||
// TODO: add button for this
|
||||
const name = prompt(`Rename tag '${tag.get_name()}':`);
|
||||
if (name === null || name.length < 1) return //cancel
|
||||
tag.name = name;
|
||||
|
||||
this.saveTags();
|
||||
this.buildTagList();
|
||||
}
|
||||
|
||||
removeTag(tag) {
|
||||
if (!confirm(`Do you want to delete ${tag.get_name()}`)) {
|
||||
return false;
|
||||
}
|
||||
// TODO: add button for this
|
||||
const request = new Request("/tags/" + tag.id);
|
||||
return fetch(request)
|
||||
.then(response => {
|
||||
if (response.status == 404) {
|
||||
return [] // not existing tag surely has no annotations
|
||||
} else {
|
||||
return response.json()
|
||||
}
|
||||
})
|
||||
.then(annotations => {
|
||||
if (annotations.length) {
|
||||
alert(`Cannot remove '${tag.get_name()}', as it is used for ${annotations.length} annotations.`)
|
||||
} else {
|
||||
// TODO: remove tag
|
||||
tag.parent.children.splice(tag.parent.children.indexOf(tag), 1);
|
||||
tag.parent = null;
|
||||
|
||||
this.saveTags();
|
||||
this.buildTagList();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setTagColor(tag, color) {
|
||||
tag.color = color;
|
||||
this.buildTagList();
|
||||
|
||||
this.saveTags();
|
||||
}
|
||||
|
||||
async moveSelectedAnnotations(tag) {
|
||||
// TODO: add button for this
|
||||
// alert(`This doesn't work yet! (move to tag ${tag.get_name()})`)
|
||||
|
||||
await Promise.all(this.selectedAnnotations.map(async (annotation) => {
|
||||
const formData = new FormData();
|
||||
formData.append('tag_id', tag.id)
|
||||
return await fetch('/annotation/'+annotation.id, {
|
||||
method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json'
|
||||
// },
|
||||
body: formData //JSON.stringify({'tag_id': tag.id})
|
||||
}).catch((e) => alert('Something went wrong saving the tags'));
|
||||
}));
|
||||
|
||||
this.loadAnnotationsForTag(this.selectedTag)
|
||||
this.loadTags() //updates the counts
|
||||
}
|
||||
|
||||
async saveTags() {
|
||||
const json = this.rootTag.export();
|
||||
console.log('save', json)
|
||||
const response = await fetch('/tags.json', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: json
|
||||
}).catch((e) => alert('Something went wrong saving the tags'));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class Tag {
|
||||
parent = null;
|
||||
children = [];
|
||||
|
||||
static from(json_obj) {
|
||||
if (json_obj.hasOwnProperty('children')) {
|
||||
json_obj.children = json_obj.children.map(Tag.from)
|
||||
} else {
|
||||
json_obj.children = [];
|
||||
}
|
||||
json_obj.parent = null;
|
||||
const tag = Object.assign(new Tag(), json_obj);
|
||||
tag.children.map((child) => child.parent = tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
is_root() {
|
||||
return this.parent === null;
|
||||
}
|
||||
|
||||
descendants(inc_self = false) {
|
||||
let tags = this.children.flatMap((t) => t.descendants(true))
|
||||
if (inc_self)
|
||||
tags.unshift(this)
|
||||
return tags;
|
||||
}
|
||||
|
||||
find_by_id(tag_id) {
|
||||
const desc = this.descendants().filter((tag) => tag.id == tag_id);
|
||||
if (desc.length) return desc[0];
|
||||
return null;
|
||||
}
|
||||
|
||||
depth() {
|
||||
if (this.parent === null) {
|
||||
return 0;
|
||||
}
|
||||
return this.parent.depth() + 1;
|
||||
}
|
||||
|
||||
root() {
|
||||
if (this.parent !== null) {
|
||||
return this.parent.root();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
get_name() {
|
||||
if (this.hasOwnProperty('name') && this.name !== null) {
|
||||
return this.name;
|
||||
}
|
||||
return this.id;
|
||||
}
|
||||
|
||||
get_indented_name() {
|
||||
const name = this.get_name();
|
||||
return ' '.repeat((this.depth() - 1) * 2) + '- ' + name
|
||||
}
|
||||
|
||||
get_color() {
|
||||
if (this.hasOwnProperty('color') && this.color !== null) {
|
||||
return this.color;
|
||||
}
|
||||
if (this.parent !== null) {
|
||||
return this.parent.get_color()
|
||||
}
|
||||
return 'black';
|
||||
}
|
||||
|
||||
_json_replacer(key, value) {
|
||||
if (key === 'children' && !value.length) {
|
||||
return undefined;
|
||||
}
|
||||
if (key === 'parent' || key === 'menuLiEl') {
|
||||
return undefined;
|
||||
}
|
||||
if (key === 'color' && this.parent !== null && this.parent.get_color() === value) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export() {
|
||||
return JSON.stringify(this, this._json_replacer)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,81 +1,172 @@
|
|||
{
|
||||
"human-machine": {
|
||||
"fullname": "Human/machine Entanglements (Appearance/disappearance)",
|
||||
"color": "orange",
|
||||
"sub": {
|
||||
"vision": {
|
||||
"fullname": "Vision",
|
||||
"color": "orange"
|
||||
},
|
||||
"sound": {},
|
||||
"behaviour": {},
|
||||
"other-senses": {
|
||||
"fullname": "Other senses"
|
||||
}
|
||||
"id": "root",
|
||||
"name": "root",
|
||||
"color": null,
|
||||
"description": "",
|
||||
"children": [
|
||||
{
|
||||
"id": "human-machine",
|
||||
"name": "Human/machine Entanglements (Appearance/disappearance)",
|
||||
"color": "#ffa348",
|
||||
"description": "",
|
||||
"children": [
|
||||
{
|
||||
"id": "vision",
|
||||
"name": "Vision",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "sound",
|
||||
"name": "sound",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "behaviour",
|
||||
"name": "behaviour",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "other-senses",
|
||||
"name": "Other senses",
|
||||
"color": null,
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"tensions": {
|
||||
"fullname": "Tensions, contestations & problems",
|
||||
"description" : "Which problems are identified?, when do they become problems?",
|
||||
"color": "gray"
|
||||
{
|
||||
"id": "tensions",
|
||||
"name": "Tensions, contestations & problems",
|
||||
"color": "#77767b",
|
||||
"description": "Which problems are identified?, when do they become problems?"
|
||||
},
|
||||
"security": {
|
||||
"color": "blue",
|
||||
"fullname": "Security & types of data",
|
||||
"sub": {
|
||||
"definitions": {
|
||||
"description": "e.g. domain knowledge"
|
||||
},
|
||||
"input": {
|
||||
"description": "e.g. fake data"
|
||||
}
|
||||
{
|
||||
"id": "security",
|
||||
"name": "Security & types of data",
|
||||
"color": "#3584e4",
|
||||
"description": "",
|
||||
"children": [
|
||||
{
|
||||
"id": "definitions",
|
||||
"name": "definitions",
|
||||
"color": null,
|
||||
"description": "e.g. domain knowledge"
|
||||
},
|
||||
{
|
||||
"id": "input",
|
||||
"name": "input",
|
||||
"color": null,
|
||||
"description": "e.g. fake data"
|
||||
}
|
||||
]
|
||||
},
|
||||
"actants":{
|
||||
"fullname": "Actants in relation",
|
||||
"color": "pink",
|
||||
"sub":{
|
||||
"algorithm": {},
|
||||
"technologies": {},
|
||||
"frt": {},
|
||||
"cameras": {
|
||||
"fullname": "CCTV & camera's"
|
||||
},
|
||||
"entities":{
|
||||
"fullname": "Entities: people, institutions etc."
|
||||
},
|
||||
"positioning":{
|
||||
"fullname": "Positioning",
|
||||
"description": "the positioning of a field/person/oneself in relation to others"
|
||||
},
|
||||
"inside-outside":{},
|
||||
"public-private":{}
|
||||
{
|
||||
"id": "actants",
|
||||
"name": "Actants in relation",
|
||||
"color": "#fa08ff",
|
||||
"description": "",
|
||||
"children": [
|
||||
{
|
||||
"id": "algorithm",
|
||||
"name": "algorithm",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "technologies",
|
||||
"name": "technologies",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "frt",
|
||||
"name": "frt",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "cameras",
|
||||
"name": "CCTV & camera's",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "entities",
|
||||
"name": "Entities: people, institutions etc.",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "positioning",
|
||||
"name": "Positioning",
|
||||
"color": null,
|
||||
"description": "the positioning of a field/person/oneself in relation to others"
|
||||
},
|
||||
{
|
||||
"id": "inside-outside",
|
||||
"name": "inside-outside",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "public-private",
|
||||
"name": "public-private",
|
||||
"color": null,
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"consequences":{
|
||||
"color": "green",
|
||||
"sub":{
|
||||
"effects":{},
|
||||
"future-imaginaries":{},
|
||||
"speculations": {
|
||||
"description": "what is & what will/can be done."
|
||||
},
|
||||
"innovations": {}
|
||||
{
|
||||
"id": "consequences",
|
||||
"name": "consequences",
|
||||
"color": "#0add32",
|
||||
"description": "",
|
||||
"children": [
|
||||
{
|
||||
"id": "effects",
|
||||
"name": "effects",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "future-imaginaries",
|
||||
"name": "future-imaginaries",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "speculations",
|
||||
"name": "speculations",
|
||||
"color": null,
|
||||
"description": "what is & what will/can be done."
|
||||
},
|
||||
{
|
||||
"id": "innovations",
|
||||
"name": "innovations",
|
||||
"color": null,
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"hesitation":{
|
||||
"fullname": "Hesitations & corrections",
|
||||
"color": "yellow"
|
||||
{
|
||||
"id": "hesitation",
|
||||
"name": "Hesitations & corrections",
|
||||
"color": "#f8e45c",
|
||||
"description": ""
|
||||
},
|
||||
|
||||
"skip":{
|
||||
"color": "black"
|
||||
{
|
||||
"id": "skip",
|
||||
"name": "skip",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
|
||||
"todo":{
|
||||
"fullname": "to do / interesting",
|
||||
"color": "red"
|
||||
{
|
||||
"id": "todo",
|
||||
"name": "to do / interesting",
|
||||
"color": "#ff0000",
|
||||
"description": ""
|
||||
}
|
||||
|
||||
]
|
||||
}
|
49
poetry.lock
generated
49
poetry.lock
generated
|
@ -1,3 +1,18 @@
|
|||
[[package]]
|
||||
name = "anytree"
|
||||
version = "2.8.0"
|
||||
description = "Powerful and Lightweight Python Tree Data Structure.."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.9.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["check-manifest"]
|
||||
test = ["coverage"]
|
||||
|
||||
[[package]]
|
||||
name = "coloredlogs"
|
||||
version = "15.0.1"
|
||||
|
@ -12,6 +27,18 @@ humanfriendly = ">=9.1"
|
|||
[package.extras]
|
||||
cron = ["capturer (>=2.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.7.1"
|
||||
description = "A platform independent file lock."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||
testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "humanfriendly"
|
||||
version = "10.0"
|
||||
|
@ -39,6 +66,14 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "svgwrite"
|
||||
version = "1.4.2"
|
||||
|
@ -58,13 +93,21 @@ python-versions = ">= 3.5"
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "b989d535550aaf74b32cd9d2ff48ab7ed8d7d3fd5f0386bc2d6385d65adbf438"
|
||||
content-hash = "11298c8670e03d4235a76555b1ca7e6cbf0740041f14c14fdf409f633e197bf5"
|
||||
|
||||
[metadata.files]
|
||||
anytree = [
|
||||
{file = "anytree-2.8.0-py2.py3-none-any.whl", hash = "sha256:14c55ac77492b11532395049a03b773d14c7e30b22aa012e337b1e983de31521"},
|
||||
{file = "anytree-2.8.0.tar.gz", hash = "sha256:3f0f93f355a91bc3e6245319bf4c1d50e3416cc7a35cc1133c1ff38306bbccab"},
|
||||
]
|
||||
coloredlogs = [
|
||||
{file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"},
|
||||
{file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"},
|
||||
]
|
||||
filelock = [
|
||||
{file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
|
||||
{file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
|
||||
]
|
||||
humanfriendly = [
|
||||
{file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"},
|
||||
{file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"},
|
||||
|
@ -77,6 +120,10 @@ pyreadline3 = [
|
|||
{file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"},
|
||||
{file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
svgwrite = [
|
||||
{file = "svgwrite-1.4.2-py3-none-any.whl", hash = "sha256:ca63d76396d1f6f099a2b2d8cf1419e1c1de8deece9a2b7f4da0632067d71d43"},
|
||||
{file = "svgwrite-1.4.2.zip", hash = "sha256:d304a929f197d31647c287c10eee0f64378058e1c83a9df83433a5980864e59f"},
|
||||
|
|
|
@ -10,6 +10,8 @@ tornado = "^6.1"
|
|||
coloredlogs = "^15.0.1"
|
||||
pydub = "^0.25.1"
|
||||
svgwrite = "^1.4.1"
|
||||
anytree = "^2.8.0"
|
||||
filelock = "^3.7.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
|
|
Loading…
Reference in a new issue