diff --git a/files/.gitignore b/files/.gitignore
deleted file mode 100644
index d6b7ef3..0000000
--- a/files/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!.gitignore
diff --git a/files/audio/.gitignore b/files/audio/.gitignore
deleted file mode 100644
index d6b7ef3..0000000
--- a/files/audio/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!.gitignore
diff --git a/svganim.service b/svganim.service
index e86264f..76001fe 100644
--- a/svganim.service
+++ b/svganim.service
@@ -1,10 +1,11 @@
[Unit]
Description=SVG animation interfaces
[Service]
-ExecStart=/home/svganim/.poetry/bin/poetry webserver.py
+ExecStart=/home/svganim/.poetry/bin/poetry run python webserver.py
WorkingDirectory=/home/svganim/svganim
User=svganim
Restart=on-failure
+Environment="PATH=/home/svganim/.pyenv/plugins/pyenv-virtualenv/shims:/home/svganim/.pyenv/shims:/home/svganim/.pyenv/bin:/home/svganim/.poetry/bin:/usr/local/bin:/usr/bin:/bin"
[Install]
WantedBy=multi-user.target
diff --git a/svganim/strokes.py b/svganim/strokes.py
index 45a4396..c911aad 100644
--- a/svganim/strokes.py
+++ b/svganim/strokes.py
@@ -8,7 +8,9 @@ from pydub import AudioSegment
import svgwrite
import tempfile
import io
+import logging
+logger = logging.getLogger('svganim.strokes')
class Annotation:
def __init__(self, tag: str, drawing: Drawing, t_in: float, t_out: float) -> None:
@@ -46,6 +48,7 @@ class Drawing:
return f"/annotations/{self.id}"
def get_canvas_metadata(self) -> list:
+ logger.info(f'metadata for {self.id}')
with open(self.eventfile, "r") as fp:
first_line = fp.readline().strip()
@@ -79,6 +82,10 @@ class Drawing:
# metadata on first line
pass
else:
+ if type(event) is list:
+ # ignore double metadatas, which appear when continuaing an existing drawing
+ continue
+
if event["event"] == "viewbox":
pass
if event["event"] == "stroke":
@@ -275,11 +282,12 @@ class AnnotationIndex:
self.drawing_dir = drawing_dir
self.metadata_dir = metadata_dir
- self.shelve = shelve.open(filename, writeback=True)
+ self.shelve = {} #disable disk cache because of glitches shelve.open(filename, writeback=True)
def refresh(self):
# reset the index
- for key in self.shelve:
+ for key in list(self.shelve.keys()):
+ print(key)
del self.shelve[key]
self.shelve["_drawings"] = {
@@ -328,7 +336,7 @@ class AnnotationIndex:
return [
name[:-16]
for name in os.listdir(self.drawing_dir)
- if name.endswith("json_appendable")
+ if name.endswith("json_appendable") and os.stat(os.path.join(self.drawing_dir, name)).st_size > 0
]
def get_drawing_filenames(self) -> list[Filename]:
@@ -346,8 +354,8 @@ class AnnotationIndex:
class Point:
def __init__(self, x: float, y: float, last: bool, t: float):
- self.x = x
- self.y = y
+ self.x = float(x)
+ self.y = float(y) # if y == 0 it can still be integer.... odd python
self.last = last
self.t = t
diff --git a/webserver.py b/webserver.py
index 888d263..8ca6c01 100644
--- a/webserver.py
+++ b/webserver.py
@@ -46,6 +46,7 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
self.config = config
self.strokes = []
self.hasWritten = False
+ self.prev_file = None
self.dimensions = [None, None]
# def check_origin(self, origin):
@@ -69,11 +70,28 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
return len(files) + 1
def appendEvent(self, row):
+ if not self.hasWritten and self.prev_file and 'event' in row and row['event'] == 'viewbox':
+ # ignore canvas movement after
+ return
+
# write to an appendable json format. So basically a file that should be wrapped in [] to be json-parsable
with open(
- os.path.join(self.config.storage, self.filename + ".json_appendable"), "a"
+ os.path.join(self.config.storage, self.filename +
+ ".json_appendable"), "a"
) as fp:
if not self.hasWritten:
+ if self.prev_file:
+ # TODO WIP
+ with open(
+ self.prev_file, 'r'
+ ) as fprev:
+ wrote = False
+ for line in fprev:
+ wrote = True
+ fp.write(line)
+ if wrote:
+ fp.write(",\n")
+
# metadata to first row, but only on demand
fp.write(
json.dumps(
@@ -91,6 +109,33 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
# first column is color, rest is points
fp.write(json.dumps(row))
+ def preloadFile(self, file):
+ if self.hasWritten:
+ logger.error("Cannot preload when already written content")
+ return False
+
+ logger.info(f"load {file}")
+ # TODO, make sure file doesn't load file outside of storage
+ prev_file = os.path.join(
+ self.config.storage, file + ".json_appendable")
+ if not os.path.exists(prev_file):
+ logger.error(f"Cannot preload non-existent file: {prev_file}")
+ self.write_message(json.dumps(
+ {"error": f"Non-existent file: {file}"}))
+ return False
+
+ self.prev_file = prev_file
+
+ with open(self.prev_file, "r") as fp:
+ first_line = fp.readline().strip()
+ if first_line.endswith(","):
+ first_line = first_line[:-1]
+
+ metadata = json.loads(first_line)
+
+ self.write_message(json.dumps(
+ {"preloaded_svg": f"/drawing/{file}", "dimensions": [metadata[1], metadata[2]]}))
+
# the client sent the message
def on_message(self, message):
logger.info(f"recieve: {message}")
@@ -106,6 +151,8 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
elif msg["event"] == "viewbox":
logger.info("move or resize")
self.appendEvent(msg)
+ elif msg["event"] == "preload":
+ self.preloadFile(msg["file"])
else:
# self.send({'alert': 'Unknown request: {}'.format(message)})
logger.warn("Unknown request: {}".format(message))
@@ -168,25 +215,34 @@ class AnimationHandler(tornado.web.RequestHandler):
if name.endswith("json_appendable")
]
for name in names:
- with open(os.path.join(self.config.storage, name), "r") as fp:
+ fn = os.path.join(self.config.storage, name)
+ stat = os.stat(fn)
+ if stat.st_size == 0:
+ continue
+
+ with open(fn, "r") as fp:
first_line = fp.readline().strip()
if first_line.endswith(","):
first_line = first_line[:-1]
- print(first_line)
+
metadata = json.loads(first_line)
files.append(
{
"name": f"/files/{name[:-16]}",
- "time": metadata[0],
+ "id": name[:-16],
+ "ctime": metadata[0],
+ "mtime": datetime.datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %T"),
"dimensions": [metadata[1], metadata[2]],
+ "svg": f"/drawing/{name[:-16]}.svg",
}
)
- files.sort(key=lambda k: k["time"])
+ files.sort(key=lambda k: k["mtime"])
self.write(json.dumps(files))
else:
path = os.path.join(
- self.config.storage, os.path.basename(filename) + ".json_appendable"
+ self.config.storage, os.path.basename(
+ filename) + ".json_appendable"
)
drawing = {"file": filename, "shape": []}
with open(path, "r") as fp:
@@ -205,7 +261,8 @@ class AnimationHandler(tornado.web.RequestHandler):
# p = stroke[i*4:i*4+4]
# points.append([float(p[0]), float(p[1]), int(p[2]), float(p[3])])
drawing["shape"].append(
- {"color": event["color"], "points": event["points"]}
+ {"color": event["color"],
+ "points": event["points"]}
)
self.write(json.dumps(drawing))
@@ -273,10 +330,12 @@ class AnnotationHandler(tornado.web.RequestHandler):
self.write(annotation.get_as_svg())
elif extension == "mp3":
self.set_header("Content-Type", "audio/mp3")
- self.write(annotation.getAnimationSlice().audio.export(format="mp3").read())
+ self.write(annotation.getAnimationSlice(
+ ).audio.export(format="mp3").read())
elif extension == "wav":
self.set_header("Content-Type", "audio/wav")
- self.write(annotation.getAnimationSlice().audio.export(format="wav").read())
+ self.write(annotation.getAnimationSlice(
+ ).audio.export(format="wav").read())
else:
self.set_header("Content-Type", "application/json")
self.write(json.dumps({
@@ -286,6 +345,57 @@ class AnnotationHandler(tornado.web.RequestHandler):
}))
+class DrawingHandler(tornado.web.RequestHandler):
+ """Get drawing as svg"""
+
+ def initialize(self, config, index: svganim.strokes.AnnotationIndex):
+ self.config = config
+ self.index = index
+ self.metadir = os.path.join(self.config.storage, "metadata")
+
+ def get(self, drawing_id):
+ if drawing_id[-4:] == ".svg":
+ extension = "svg"
+ drawing_id = drawing_id[:-4]
+ elif drawing_id[-4:] == ".mp3":
+ extension = "mp3"
+ drawing_id = drawing_id[:-4]
+ elif drawing_id[-4:] == ".wav":
+ extension = "wav"
+ drawing_id = drawing_id[:-4]
+ else:
+ extension = None
+
+ logger.info(f"drawing {drawing_id=}, {extension=}")
+ if drawing_id not in self.index.drawings:
+ self.index.refresh()
+ # double check
+ if drawing_id not in self.index.drawings:
+ raise tornado.web.HTTPError(404)
+
+ drawing = self.index.drawings[drawing_id]
+
+ if extension == "svg":
+ self.set_header("Content-Type", "image/svg+xml")
+ self.write(drawing.get_animation().get_as_svg())
+ elif extension == "mp3":
+ self.set_header("Content-Type", "audio/mp3")
+ self.write(drawing.get_animation(
+ ).audio.export(format="mp3").read())
+ elif extension == "wav":
+ self.set_header("Content-Type", "audio/wav")
+ self.write(drawing.get_animation(
+ ).audio.export(format="wav").read())
+ else:
+ self.set_header("Content-Type", "application/json")
+ self.write(json.dumps({
+ "id": drawing.id,
+ "annotations_url": drawing.get_annotations_url(),
+ "audio": f"/drawing/{drawing.id}.mp3",
+ "svg": f"/drawing/{drawing.id}.svg",
+ }))
+
+
class AnnotationsHandler(tornado.web.RequestHandler):
def initialize(self, config):
self.config = config
@@ -337,6 +447,7 @@ class AnnotationsHandler(tornado.web.RequestHandler):
with open(meta_file, "w") as fp:
json.dump(self.json_args, fp)
+
class IndexHandler(tornado.web.RequestHandler):
"""Get annotation as svg"""
@@ -350,9 +461,10 @@ class IndexHandler(tornado.web.RequestHandler):
self.logger.info("Reloading Annotation Index")
self.index.refresh()
self.logger.info("\treloaded annotation index")
-
+
self.render("templates/index.html", index=self.index)
+
class Server:
"""
Server for HIT -> plotter events
@@ -392,8 +504,10 @@ class Server:
{"path": os.path.join(self.config.storage, "audio")},
),
(r"/audio", AudioListingHandler, {"config": self.config}),
- (r"/annotations/(.+)", AnnotationsHandler, {"config": self.config}),
- (r"/tags", TagHandler, {"config": self.config, "index": self.index}),
+ (r"/annotations/(.+)", AnnotationsHandler,
+ {"config": self.config}),
+ (r"/tags", TagHandler,
+ {"config": self.config, "index": self.index}),
(
r"/tags/(.+)",
TagAnnotationsHandler,
@@ -404,9 +518,16 @@ class Server:
AnnotationHandler,
{"config": self.config, "index": self.index},
),
- (r"/(.+)", StaticFileWithHeaderHandler, {"path": self.web_root}),
+ (
+ r"/drawing/(.+)",
+ DrawingHandler,
+ {"config": self.config, "index": self.index},
+ ),
+ (r"/(.+)", StaticFileWithHeaderHandler,
+ {"path": self.web_root}),
- (r"/", IndexHandler, {"config": self.config, "index": self.index}),
+ (r"/", IndexHandler,
+ {"config": self.config, "index": self.index}),
],
debug=True,
autoreload=True,
diff --git a/www/annotate.html b/www/annotate.html
index d33fea0..045f46d 100644
--- a/www/annotate.html
+++ b/www/annotate.html
@@ -193,6 +193,21 @@
.annotation-google {
background-color: blueviolet !important;
}
+ .annotation-map {
+ background-color: red !important;
+ }
+
+ .annotation-relation {
+ background-color: blue !important;
+ }
+
+ .annotation-text {
+ background-color: blueviolet !important;
+ }
+
+ .annotation-figure {
+ background-color: pink !important;
+ }
.unsaved::before {
content: '*';
@@ -240,6 +255,14 @@
width: 100px; /* hides seek head */
}
+ .playlist img{
+ position: static;
+ width: 250px;
+ height: 250px;
+ background: white;
+ display: block;
+ }
+
@@ -257,7 +280,7 @@
if (location.search) {
ann = new Annotator(
document.getElementById("interface"),
- ["test", "another", "google"],
+ ["map", "text", "relation", "figure"],
location.search.substring(1)
);
} else {
diff --git a/www/draw.html b/www/draw.html
index 24995ed..63ac0f2 100644
--- a/www/draw.html
+++ b/www/draw.html
@@ -34,7 +34,7 @@
path {
fill: none;
- stroke: red;
+ stroke: auto;
stroke-width: 1mm;
stroke-linecap: round;
}
@@ -60,7 +60,7 @@
margin: 0;
font-family: sans-serif;
/* prevent reload on scroll in chrome */
- position: fixed;
+ position: fixed;
overscroll-behavior: contain;
overflow-y: hidden;
@@ -170,7 +170,25 @@