Drawing extraction and some fixes
This commit is contained in:
parent
d04ccd3c5b
commit
2c5d105e7f
4 changed files with 378 additions and 172 deletions
|
@ -5,7 +5,7 @@ description = "Draw an animated vector image"
|
||||||
authors = ["Ruben van de Ven <git@rubenvandeven.com>"]
|
authors = ["Ruben van de Ven <git@rubenvandeven.com>"]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.9"
|
python = "^3.7"
|
||||||
tornado = "^6.1"
|
tornado = "^6.1"
|
||||||
coloredlogs = "^15.0.1"
|
coloredlogs = "^15.0.1"
|
||||||
pydub = "^0.25.1"
|
pydub = "^0.25.1"
|
||||||
|
|
11
svganim.service
Normal file
11
svganim.service
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[Unit]
|
||||||
|
Description=SVG animation interfaces
|
||||||
|
[Service]
|
||||||
|
ExecStart=/home/svganim/.poetry/bin/poetry webserver.py
|
||||||
|
WorkingDirectory=/home/svganim/svganim
|
||||||
|
User=svganim
|
||||||
|
Restart=on-failure
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,12 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
from os import X_OK, PathLike
|
from os import X_OK, PathLike
|
||||||
import os
|
import os
|
||||||
from webserver import strokes2D
|
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
import shelve
|
import shelve
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
import svgwrite
|
import svgwrite
|
||||||
|
import tempfile
|
||||||
|
import io
|
||||||
|
|
||||||
|
|
||||||
class Annotation:
|
class Annotation:
|
||||||
|
@ -16,28 +17,27 @@ class Annotation:
|
||||||
self.t_out = t_out
|
self.t_out = t_out
|
||||||
self.drawing = drawing
|
self.drawing = drawing
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self) -> str:
|
||||||
|
return f'{self.drawing.id}:{self.tag}:{self.t_in}:{self.t_out}'
|
||||||
|
|
||||||
|
|
||||||
def getAnimationSlice(self) -> AnimationSlice:
|
def getAnimationSlice(self) -> AnimationSlice:
|
||||||
return self.drawing.get_animation().getSlice(self.t_in, self.t_out)
|
return self.drawing.get_animation().getSlice(self.t_in, self.t_out)
|
||||||
|
|
||||||
def get_as_svg(self):
|
def get_as_svg(self) -> str:
|
||||||
anim = self.getAnimationSlice()
|
return self.getAnimationSlice().get_as_svg()
|
||||||
bb = anim.get_bounding_box()
|
|
||||||
dwg = svgwrite.Drawing('/tmp/test.svg', size=(bb.width, bb.height))
|
|
||||||
anim.add_to_dwg(dwg)
|
|
||||||
dwg.save()
|
|
||||||
# TODO .... wip
|
|
||||||
print('saved!')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Filename = Union[str, bytes, PathLike[str], PathLike[bytes]]
|
Filename = Union[str, bytes, PathLike[str], PathLike[bytes]]
|
||||||
|
|
||||||
|
|
||||||
class Drawing:
|
class Drawing:
|
||||||
def __init__(self, filename: Filename, metadata_dir: Filename) -> None:
|
def __init__(self, filename: Filename, metadata_dir: Filename, basedir: Filename) -> None:
|
||||||
self.eventfile = filename
|
self.eventfile = filename
|
||||||
self.id = os.path.splitext(os.path.basename(self.eventfile))[0]
|
self.id = os.path.splitext(os.path.basename(self.eventfile))[0]
|
||||||
self.metadata_fn = os.path.join(metadata_dir, f"{self.id}.json")
|
self.metadata_fn = os.path.join(metadata_dir, f"{self.id}.json")
|
||||||
|
self.basedir = basedir
|
||||||
|
|
||||||
def get_url(self) -> str:
|
def get_url(self) -> str:
|
||||||
return f"/files/{self.id}"
|
return f"/files/{self.id}"
|
||||||
|
@ -60,6 +60,15 @@ class Drawing:
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_audio(self) -> Optional[AudioSlice]:
|
||||||
|
md = self.get_metadata()
|
||||||
|
if 'audio' not in md:
|
||||||
|
return None
|
||||||
|
if 'file' not in md['audio']:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return AudioSlice(filename=os.path.join(self.basedir,md['audio']['file'][1:]), offset=md['audio']['offset']*1000)
|
||||||
|
|
||||||
def get_animation(self) -> AnimationSlice:
|
def get_animation(self) -> AnimationSlice:
|
||||||
# with open(self.eventfile, "r") as fp:
|
# with open(self.eventfile, "r") as fp:
|
||||||
strokes = []
|
strokes = []
|
||||||
|
@ -80,10 +89,10 @@ class Drawing:
|
||||||
strokes.append(
|
strokes.append(
|
||||||
Stroke(
|
Stroke(
|
||||||
event["color"],
|
event["color"],
|
||||||
[Point.fromTuple(tuple(p)) for p in event["points"]],
|
[Point.fromTuple(tuple(p)).scaled(self.get_canvas_metadata()['dimensions']) for p in event["points"]],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return AnimationSlice(strokes)
|
return AnimationSlice(strokes, audioslice=self.get_audio() )
|
||||||
|
|
||||||
def get_metadata(self):
|
def get_metadata(self):
|
||||||
canvas = self.get_canvas_metadata()
|
canvas = self.get_canvas_metadata()
|
||||||
|
@ -117,11 +126,12 @@ class AnimationSlice:
|
||||||
# either a whole drawing or the result of applying an annotation to a drawing (an excerpt)
|
# either a whole drawing or the result of applying an annotation to a drawing (an excerpt)
|
||||||
# TODO rename to AnimationSlice to include audio as well
|
# TODO rename to AnimationSlice to include audio as well
|
||||||
def __init__(
|
def __init__(
|
||||||
self, strokes: list[Stroke], t_in: float = 0, t_out: float = None
|
self, strokes: list[Stroke], t_in: float = 0, t_out: float = None, audioslice: AudioSlice = None
|
||||||
) -> None:
|
) -> None:
|
||||||
self.strokes = strokes
|
self.strokes = strokes
|
||||||
self.t_in = t_in
|
self.t_in = t_in
|
||||||
self.t_out = t_out
|
self.t_out = t_out
|
||||||
|
self.audio = audioslice
|
||||||
# TODO: Audio
|
# TODO: Audio
|
||||||
|
|
||||||
def get_bounding_box(self) -> Viewbox:
|
def get_bounding_box(self) -> Viewbox:
|
||||||
|
@ -144,12 +154,29 @@ class AnimationSlice:
|
||||||
frame_in = self.getIndexForTime(t_in)
|
frame_in = self.getIndexForTime(t_in)
|
||||||
frame_out = self.getIndexForTime(t_out)
|
frame_out = self.getIndexForTime(t_out)
|
||||||
strokes = self.getStrokeSlices(frame_in, frame_out)
|
strokes = self.getStrokeSlices(frame_in, frame_out)
|
||||||
return AnimationSlice(strokes, t_in, t_out)
|
audio = self.audio.getSlice(t_in, t_out) if self.audio else None
|
||||||
|
return AnimationSlice(strokes, t_in, t_out, audio)
|
||||||
|
|
||||||
|
|
||||||
|
def get_as_svg_dwg(self) -> svgwrite.Drawing:
|
||||||
|
box = self.get_bounding_box()
|
||||||
|
(_, fn) = tempfile.mkstemp(suffix='.svg', text=True)
|
||||||
|
dwg = svgwrite.Drawing(fn, size=(box.width, box.height))
|
||||||
|
dwg.viewbox(box.x, box.y, box.width, box.height)
|
||||||
|
self.add_to_dwg(dwg)
|
||||||
|
return dwg
|
||||||
|
|
||||||
|
def get_as_svg(self) -> str:
|
||||||
|
dwg = self.get_as_svg_dwg()
|
||||||
|
fp = io.StringIO()
|
||||||
|
dwg.write(fp, pretty=True)
|
||||||
|
return fp.getvalue()
|
||||||
|
|
||||||
def add_to_dwg(self, dwg: SvgDrawing):
|
def add_to_dwg(self, dwg: SvgDrawing):
|
||||||
group = svgwrite.container.Group()
|
group = svgwrite.container.Group()
|
||||||
for stroke in self.strokes:
|
for stroke in self.strokes:
|
||||||
stroke.add_to_dwg(group)
|
stroke.add_to_dwg(group)
|
||||||
|
dwg.add(group)
|
||||||
|
|
||||||
def getStrokeSlices(
|
def getStrokeSlices(
|
||||||
self, index_in: FrameIndex, index_out: FrameIndex
|
self, index_in: FrameIndex, index_out: FrameIndex
|
||||||
|
@ -200,16 +227,41 @@ class AnimationSlice:
|
||||||
|
|
||||||
|
|
||||||
class AudioSlice:
|
class AudioSlice:
|
||||||
def __init__(self, filename: Filename, t_in: float, t_out: float):
|
def __init__(self, filename: Filename, t_in: float=None, t_out: float=None, offset:float = None):
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.t_in = t_in # in ms
|
self.t_in = t_in # in ms
|
||||||
self.t_out = t_out # in ms
|
self.t_out = t_out # in ms
|
||||||
|
self.offset = offset # in ms
|
||||||
|
|
||||||
|
def getSlice(self, t_in: float, t_out: float) -> AnimationSlice:
|
||||||
|
return AudioSlice(self.filename, t_in, t_out, self.offset)
|
||||||
|
|
||||||
def export(self, format="mp3"):
|
def export(self, format="mp3"):
|
||||||
"""Returns file descriptor of tempfile"""
|
"""Returns file descriptor of tempfile"""
|
||||||
# Opening file and extracting segment
|
# Opening file and extracting segment
|
||||||
song = AudioSegment.from_file(self.filename)
|
song = AudioSegment.from_file(self.filename)
|
||||||
extract = song[self.t_in : self.t_out]
|
start = self.t_in - self.offset
|
||||||
|
end = self.t_out - self.offset
|
||||||
|
|
||||||
|
if start < 0 and end < 0:
|
||||||
|
extract = AudioSegment.silent(duration=end-start, frame_rate=song.frame_rate)
|
||||||
|
else:
|
||||||
|
if start < 0:
|
||||||
|
preroll = AudioSegment.silent(duration=start * -1, frame_rate=song.frame_rate)
|
||||||
|
start = 0
|
||||||
|
else:
|
||||||
|
preroll = None
|
||||||
|
if end > len(song):
|
||||||
|
postroll = AudioSegment.silent(duration=end - len(song), frame_rate=song.frame_rate)
|
||||||
|
end = len(song) - 1
|
||||||
|
else:
|
||||||
|
postroll = None
|
||||||
|
|
||||||
|
extract = song[start: end]
|
||||||
|
if preroll:
|
||||||
|
extract = preroll + extract
|
||||||
|
if postroll:
|
||||||
|
extract += postroll
|
||||||
|
|
||||||
# Saving
|
# Saving
|
||||||
return extract.export(None, format=format)
|
return extract.export(None, format=format)
|
||||||
|
@ -223,42 +275,54 @@ class AnnotationIndex:
|
||||||
self.drawing_dir = drawing_dir
|
self.drawing_dir = drawing_dir
|
||||||
self.metadata_dir = metadata_dir
|
self.metadata_dir = metadata_dir
|
||||||
|
|
||||||
self.index = shelve.open(filename)
|
self.shelve = shelve.open(filename, writeback=True)
|
||||||
|
|
||||||
def initiate(self):
|
def refresh(self):
|
||||||
# reset the index
|
# reset the index
|
||||||
for key in self.index:
|
for key in self.shelve:
|
||||||
del self.index[key]
|
del self.shelve[key]
|
||||||
|
|
||||||
self.index["_drawings"] = {
|
self.shelve["_drawings"] = {
|
||||||
d.id: d
|
d.id: d
|
||||||
for d in [
|
for d in [
|
||||||
Drawing(fn, self.metadata_dir) for fn in self.get_drawing_filenames()
|
Drawing(fn, self.metadata_dir, self.drawing_dir) for fn in self.get_drawing_filenames()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
self.shelve['_tags'] = {}
|
||||||
|
self.shelve['_annotations'] = {}
|
||||||
|
|
||||||
drawing: Drawing
|
drawing: Drawing
|
||||||
for drawing in self.index['_drawings'].values():
|
for drawing in self.shelve['_drawings'].values():
|
||||||
meta = drawing.get_metadata()
|
meta = drawing.get_metadata()
|
||||||
if 'annotations' not in meta:
|
if 'annotations' not in meta:
|
||||||
continue
|
continue
|
||||||
for ann in meta['annotations']:
|
for ann in meta['annotations']:
|
||||||
annotation = Annotation(ann['tag'], drawing, ann['t_in'], ann['t_out'])
|
annotation = Annotation(ann['tag'], drawing, ann['t_in'], ann['t_out'])
|
||||||
if annotation.tag not in self.index:
|
self.shelve['_annotations'][annotation.id] = annotation
|
||||||
self.index[annotation.tag] = [annotation]
|
if annotation.tag not in self.shelve['_tags']:
|
||||||
|
self.shelve['_tags'][annotation.tag] = [annotation]
|
||||||
else:
|
else:
|
||||||
self.index[annotation.tag].append(
|
self.shelve['_tags'][annotation.tag].append(
|
||||||
annotation
|
annotation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def drawings(self) -> dict[str, Drawing]:
|
def drawings(self) -> dict[str, Drawing]:
|
||||||
return self.index["_drawings"]
|
return self.shelve["_drawings"]
|
||||||
|
|
||||||
def get(self, tag) -> list[Annotation]:
|
@property
|
||||||
if tag not in self.index:
|
def tags(self) -> dict[str, list[Annotation]]:
|
||||||
|
return self.shelve["_tags"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def annotations(self) -> dict[str, Annotation]:
|
||||||
|
return self.shelve["_annotations"]
|
||||||
|
|
||||||
|
def get_annotations(self, tag) -> list[Annotation]:
|
||||||
|
if tag not in self.tags:
|
||||||
return []
|
return []
|
||||||
return self.index[tag]
|
return self.tags[tag]
|
||||||
|
|
||||||
def get_drawing_names(self) -> list[str]:
|
def get_drawing_names(self) -> list[str]:
|
||||||
return [
|
return [
|
||||||
|
@ -274,7 +338,7 @@ class AnnotationIndex:
|
||||||
]
|
]
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.index.close()
|
self.shelve.close()
|
||||||
|
|
||||||
|
|
||||||
# Point = tuple[float, float, float]
|
# Point = tuple[float, float, float]
|
||||||
|
@ -291,6 +355,9 @@ class Point:
|
||||||
def fromTuple(cls, p: tuple[float, float, int, float]):
|
def fromTuple(cls, p: tuple[float, float, int, float]):
|
||||||
return cls(p[0], p[1], bool(p[2]), p[3])
|
return cls(p[0], p[1], bool(p[2]), p[3])
|
||||||
|
|
||||||
|
def scaled(self, dimensions: dict[str, float]) -> Point:
|
||||||
|
return Point(self.x*dimensions['width'], self.y * dimensions['height'], self.last, self.t)
|
||||||
|
|
||||||
|
|
||||||
Points = list[Point]
|
Points = list[Point]
|
||||||
SvgDrawing = Union[svgwrite.container.SVG, svgwrite.container.Group]
|
SvgDrawing = Union[svgwrite.container.SVG, svgwrite.container.Group]
|
||||||
|
@ -301,7 +368,7 @@ class Stroke:
|
||||||
self.points = points
|
self.points = points
|
||||||
|
|
||||||
def add_to_dwg(self, dwg: SvgDrawing):
|
def add_to_dwg(self, dwg: SvgDrawing):
|
||||||
path = svgwrite.path.Path(self.get_as_d).stroke(self.color,1).fill("none")
|
path = svgwrite.path.Path(d=self.get_as_d()).stroke(self.color,1).fill("none")
|
||||||
dwg.add(path)
|
dwg.add(path)
|
||||||
|
|
||||||
|
|
||||||
|
@ -341,7 +408,7 @@ class Stroke:
|
||||||
"y": point.y - prev_point.y,
|
"y": point.y - prev_point.y,
|
||||||
}
|
}
|
||||||
# TODO multiply points by scalars for dimensions (height widht of drawing)
|
# TODO multiply points by scalars for dimensions (height widht of drawing)
|
||||||
d += f'{diff_point.x:.6},{diff_point.y:.6} '
|
d += f'{diff_point["x"]:.6},{diff_point["y"]:.6} '
|
||||||
prev_point = point
|
prev_point = point
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@ -362,3 +429,26 @@ class StrokeSlice(Stroke):
|
||||||
@property
|
@property
|
||||||
def color(self) -> str:
|
def color(self) -> str:
|
||||||
return self.stroke.color
|
return self.stroke.color
|
||||||
|
|
||||||
|
|
||||||
|
def strokes2D(strokes):
|
||||||
|
# strokes to a d attribute for a path
|
||||||
|
d = "";
|
||||||
|
last_stroke = None;
|
||||||
|
cmd = "";
|
||||||
|
for stroke in strokes:
|
||||||
|
if not last_stroke:
|
||||||
|
d += f"M{stroke[0]},{stroke[1]} "
|
||||||
|
cmd = 'M'
|
||||||
|
else:
|
||||||
|
if last_stroke[2] == 1:
|
||||||
|
d += " m"
|
||||||
|
cmd = 'm'
|
||||||
|
elif cmd != 'l':
|
||||||
|
d+=' l '
|
||||||
|
cmd = 'l'
|
||||||
|
|
||||||
|
rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
|
||||||
|
d += f"{rel_stroke[0]},{rel_stroke[1]} "
|
||||||
|
last_stroke = stroke
|
||||||
|
return d
|
339
webserver.py
339
webserver.py
|
@ -11,30 +11,34 @@ import html
|
||||||
import argparse
|
import argparse
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
import glob
|
import glob
|
||||||
|
import svganim.strokes
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("svganim.webserver")
|
||||||
|
|
||||||
|
|
||||||
class DateTimeEncoder(json.JSONEncoder):
|
class DateTimeEncoder(json.JSONEncoder):
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
if isinstance(o, datetime.datetime):
|
if isinstance(o, datetime.datetime):
|
||||||
return o.isoformat(timespec='milliseconds')
|
return o.isoformat(timespec="milliseconds")
|
||||||
|
|
||||||
return super().default(self, o)
|
return super().default(self, o)
|
||||||
|
|
||||||
|
|
||||||
class StaticFileWithHeaderHandler(tornado.web.StaticFileHandler):
|
class StaticFileWithHeaderHandler(tornado.web.StaticFileHandler):
|
||||||
def set_extra_headers(self, path):
|
def set_extra_headers(self, path):
|
||||||
"""For subclass to add extra headers to the response"""
|
"""For subclass to add extra headers to the response"""
|
||||||
if path[-5:] == '.html':
|
if path[-5:] == ".html":
|
||||||
self.set_header("Access-Control-Allow-Origin", "*")
|
self.set_header("Access-Control-Allow-Origin", "*")
|
||||||
if path[-4:] == '.svg':
|
if path[-4:] == ".svg":
|
||||||
self.set_header("Content-Type", "image/svg+xml")
|
self.set_header("Content-Type", "image/svg+xml")
|
||||||
|
|
||||||
|
|
||||||
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
"""
|
"""
|
||||||
Websocket from the workers
|
Websocket from the drawing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# CORS_ORIGINS = ['localhost']
|
# CORS_ORIGINS = ['localhost']
|
||||||
connections = set()
|
connections = set()
|
||||||
|
|
||||||
|
@ -53,27 +57,37 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
# the client connected
|
# the client connected
|
||||||
def open(self, p=None):
|
def open(self, p=None):
|
||||||
self.__class__.connections.add(self)
|
self.__class__.connections.add(self)
|
||||||
self.prefix = datetime.datetime.now().strftime('%Y-%m-%d-')
|
self.prefix = datetime.datetime.now().strftime("%Y-%m-%d-")
|
||||||
self.filename = self.prefix + str(self.check_filenr()) + '-' + uuid.uuid4().hex[:6]
|
self.filename = (
|
||||||
|
self.prefix + str(self.check_filenr()) + "-" + uuid.uuid4().hex[:6]
|
||||||
|
)
|
||||||
logger.info(f"{self.filename=}")
|
logger.info(f"{self.filename=}")
|
||||||
self.write_message(json.dumps({
|
self.write_message(json.dumps({"filename": self.filename}))
|
||||||
"filename": self.filename
|
|
||||||
}))
|
|
||||||
|
|
||||||
def check_filenr(self):
|
def check_filenr(self):
|
||||||
files = glob.glob(os.path.join(self.config.storage, self.prefix +'*'))
|
files = glob.glob(os.path.join(self.config.storage, self.prefix + "*"))
|
||||||
return len(files) + 1
|
return len(files) + 1
|
||||||
|
|
||||||
def appendEvent(self, row):
|
def appendEvent(self, row):
|
||||||
# write to an appendable json format. So basically a file that should be wrapped in [] to be json-parsable
|
# 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') as fp:
|
with open(
|
||||||
|
os.path.join(self.config.storage, self.filename + ".json_appendable"), "a"
|
||||||
|
) as fp:
|
||||||
if not self.hasWritten:
|
if not self.hasWritten:
|
||||||
# metadata to first row, but only on demand
|
# metadata to first row, but only on demand
|
||||||
fp.write(json.dumps([datetime.datetime.now().strftime("%Y-%m-%d %T"), self.dimensions[0], self.dimensions[1]]))
|
fp.write(
|
||||||
|
json.dumps(
|
||||||
|
[
|
||||||
|
datetime.datetime.now().strftime("%Y-%m-%d %T"),
|
||||||
|
self.dimensions[0],
|
||||||
|
self.dimensions[1],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
# writer.writerow()
|
# writer.writerow()
|
||||||
self.hasWritten = True
|
self.hasWritten = True
|
||||||
|
|
||||||
fp.write(',\n')
|
fp.write(",\n")
|
||||||
# first column is color, rest is points
|
# first column is color, rest is points
|
||||||
fp.write(json.dumps(row))
|
fp.write(json.dumps(row))
|
||||||
|
|
||||||
|
@ -83,18 +97,18 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = json.loads(message)
|
msg = json.loads(message)
|
||||||
if msg['event'] == 'stroke':
|
if msg["event"] == "stroke":
|
||||||
logger.info('stroke')
|
logger.info("stroke")
|
||||||
self.appendEvent(msg)
|
self.appendEvent(msg)
|
||||||
elif msg['event'] == 'dimensions':
|
elif msg["event"] == "dimensions":
|
||||||
self.dimensions = [int(msg['width']), int(msg['height'])]
|
self.dimensions = [int(msg["width"]), int(msg["height"])]
|
||||||
logger.info(f"{self.dimensions=}")
|
logger.info(f"{self.dimensions=}")
|
||||||
elif msg['event'] == 'viewbox':
|
elif msg["event"] == "viewbox":
|
||||||
logger.info('move or resize')
|
logger.info("move or resize")
|
||||||
self.appendEvent(msg)
|
self.appendEvent(msg)
|
||||||
else:
|
else:
|
||||||
# self.send({'alert': 'Unknown request: {}'.format(message)})
|
# self.send({'alert': 'Unknown request: {}'.format(message)})
|
||||||
logger.warn('Unknown request: {}'.format(message))
|
logger.warn("Unknown request: {}".format(message))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# self.send({'alert': 'Invalid request: {}'.format(e)})
|
# self.send({'alert': 'Invalid request: {}'.format(e)})
|
||||||
|
@ -106,7 +120,6 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
|
||||||
logger.info(f"Client disconnected: {self.request.remote_ip}")
|
logger.info(f"Client disconnected: {self.request.remote_ip}")
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def rmConnection(cls, client):
|
def rmConnection(cls, client):
|
||||||
if client not in cls.connections:
|
if client not in cls.connections:
|
||||||
|
@ -121,7 +134,7 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
class AudioListingHandler(tornado.web.RequestHandler):
|
class AudioListingHandler(tornado.web.RequestHandler):
|
||||||
def initialize(self, config):
|
def initialize(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.audiodir = os.path.join(self.config.storage, 'audio')
|
self.audiodir = os.path.join(self.config.storage, "audio")
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
# filename = self.get_argument("file", None)
|
# filename = self.get_argument("file", None)
|
||||||
|
@ -129,7 +142,13 @@ class AudioListingHandler(tornado.web.RequestHandler):
|
||||||
if not os.path.exists(self.audiodir):
|
if not os.path.exists(self.audiodir):
|
||||||
names = []
|
names = []
|
||||||
else:
|
else:
|
||||||
names = sorted([f"/audio/{name}" for name in os.listdir(self.audiodir) if name not in ['.gitignore']])
|
names = sorted(
|
||||||
|
[
|
||||||
|
f"/audio/{name}"
|
||||||
|
for name in os.listdir(self.audiodir)
|
||||||
|
if name not in [".gitignore"]
|
||||||
|
]
|
||||||
|
)
|
||||||
print(names)
|
print(names)
|
||||||
self.write(json.dumps(names))
|
self.write(json.dumps(names))
|
||||||
|
|
||||||
|
@ -141,79 +160,136 @@ class AnimationHandler(tornado.web.RequestHandler):
|
||||||
def get(self, filename):
|
def get(self, filename):
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
# filename = self.get_argument("file", None)
|
# filename = self.get_argument("file", None)
|
||||||
if filename == '':
|
if filename == "":
|
||||||
files = []
|
files = []
|
||||||
names = [name for name in os.listdir(self.config.storage) if name.endswith('json_appendable')]
|
names = [
|
||||||
|
name
|
||||||
|
for name in os.listdir(self.config.storage)
|
||||||
|
if name.endswith("json_appendable")
|
||||||
|
]
|
||||||
for name in names:
|
for name in names:
|
||||||
with open(os.path.join(self.config.storage, name), 'r') as fp:
|
with open(os.path.join(self.config.storage, name), "r") as fp:
|
||||||
first_line = fp.readline().strip()
|
first_line = fp.readline().strip()
|
||||||
if first_line.endswith(','):
|
if first_line.endswith(","):
|
||||||
first_line = first_line[:-1]
|
first_line = first_line[:-1]
|
||||||
print(first_line)
|
print(first_line)
|
||||||
metadata = json.loads(first_line)
|
metadata = json.loads(first_line)
|
||||||
files.append({
|
files.append(
|
||||||
'name': f"/files/{name[:-16]}",
|
{
|
||||||
|
"name": f"/files/{name[:-16]}",
|
||||||
"time": metadata[0],
|
"time": metadata[0],
|
||||||
"dimensions": [metadata[1], metadata[2]],
|
"dimensions": [metadata[1], metadata[2]],
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
files.sort(key=lambda k: k['time'])
|
files.sort(key=lambda k: k["time"])
|
||||||
self.write(json.dumps(files))
|
self.write(json.dumps(files))
|
||||||
else:
|
else:
|
||||||
path = os.path.join(self.config.storage,os.path.basename(filename)+".json_appendable")
|
path = os.path.join(
|
||||||
drawing = {
|
self.config.storage, os.path.basename(filename) + ".json_appendable"
|
||||||
"file": filename,
|
)
|
||||||
"shape": []
|
drawing = {"file": filename, "shape": []}
|
||||||
}
|
with open(path, "r") as fp:
|
||||||
with open(path, 'r') as fp:
|
events = json.loads("[" + fp.read() + "]")
|
||||||
events = json.loads('['+fp.read()+']')
|
|
||||||
for i, event in enumerate(events):
|
for i, event in enumerate(events):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
# metadata on first line
|
# metadata on first line
|
||||||
drawing['time'] = event[0]
|
drawing["time"] = event[0]
|
||||||
drawing['dimensions'] = [event[1], event[2]]
|
drawing["dimensions"] = [event[1], event[2]]
|
||||||
else:
|
else:
|
||||||
if event['event'] == 'viewbox':
|
if event["event"] == "viewbox":
|
||||||
pass
|
pass
|
||||||
if event['event'] == 'stroke':
|
if event["event"] == "stroke":
|
||||||
# points = []
|
# points = []
|
||||||
# for i in range(int(len(stroke) / 4)):
|
# for i in range(int(len(stroke) / 4)):
|
||||||
# p = stroke[i*4:i*4+4]
|
# p = stroke[i*4:i*4+4]
|
||||||
# points.append([float(p[0]), float(p[1]), int(p[2]), float(p[3])])
|
# points.append([float(p[0]), float(p[1]), int(p[2]), float(p[3])])
|
||||||
drawing['shape'].append({
|
drawing["shape"].append(
|
||||||
'color': event['color'],
|
{"color": event["color"], "points": event["points"]}
|
||||||
'points': event['points']
|
)
|
||||||
})
|
|
||||||
self.write(json.dumps(drawing))
|
self.write(json.dumps(drawing))
|
||||||
|
|
||||||
def strokes2D(strokes):
|
|
||||||
# strokes to a d attribute for a path
|
class TagHandler(tornado.web.RequestHandler):
|
||||||
d = "";
|
"""List all tags"""
|
||||||
last_stroke = None;
|
|
||||||
cmd = "";
|
def initialize(self, config, index: svganim.strokes.AnnotationIndex):
|
||||||
for stroke in strokes:
|
self.config = config
|
||||||
if not last_stroke:
|
self.index = index
|
||||||
d += f"M{stroke[0]},{stroke[1]} "
|
self.metadir = os.path.join(self.config.storage, "metadata")
|
||||||
cmd = 'M'
|
|
||||||
|
def get(self):
|
||||||
|
self.set_header("Content-Type", "application/json")
|
||||||
|
tags = self.index.tags.keys()
|
||||||
|
self.write(json.dumps(list(tags)))
|
||||||
|
|
||||||
|
|
||||||
|
class TagAnnotationsHandler(tornado.web.RequestHandler):
|
||||||
|
"""List all annotations for given tag"""
|
||||||
|
|
||||||
|
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, tag):
|
||||||
|
if tag not in self.index.tags:
|
||||||
|
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])))
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationHandler(tornado.web.RequestHandler):
|
||||||
|
"""Get annotation 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, annotation_id):
|
||||||
|
if annotation_id[-4:] == ".svg":
|
||||||
|
extension = "svg"
|
||||||
|
annotation_id = annotation_id[:-4]
|
||||||
|
elif annotation_id[-4:] == ".mp3":
|
||||||
|
extension = "mp3"
|
||||||
|
annotation_id = annotation_id[:-4]
|
||||||
|
elif annotation_id[-4:] == ".wav":
|
||||||
|
extension = "wav"
|
||||||
|
annotation_id = annotation_id[:-4]
|
||||||
else:
|
else:
|
||||||
if last_stroke[2] == 1:
|
extension = None
|
||||||
d += " m"
|
|
||||||
cmd = 'm'
|
|
||||||
elif cmd != 'l':
|
|
||||||
d+=' l '
|
|
||||||
cmd = 'l'
|
|
||||||
|
|
||||||
rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
|
logger.info(f"annotation {annotation_id=}, {extension=}")
|
||||||
d += f"{rel_stroke[0]},{rel_stroke[1]} "
|
if annotation_id not in self.index.annotations:
|
||||||
last_stroke = stroke
|
raise tornado.web.HTTPError(404)
|
||||||
return d
|
|
||||||
|
|
||||||
|
annotation = self.index.annotations[annotation_id]
|
||||||
|
|
||||||
|
if extension == "svg":
|
||||||
|
self.set_header("Content-Type", "image/svg+xml")
|
||||||
|
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())
|
||||||
|
elif extension == "wav":
|
||||||
|
self.set_header("Content-Type", "audio/wav")
|
||||||
|
self.write(annotation.getAnimationSlice().audio.export(format="wav").read())
|
||||||
|
else:
|
||||||
|
self.set_header("Content-Type", "application/json")
|
||||||
|
self.write(json.dumps({
|
||||||
|
"id": annotation.id,
|
||||||
|
"tag": annotation.tag,
|
||||||
|
"audio": f"/annotation/{annotation.id}.mp3",
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
class AnnotationsHandler(tornado.web.RequestHandler):
|
class AnnotationsHandler(tornado.web.RequestHandler):
|
||||||
def initialize(self, config):
|
def initialize(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.metadir = os.path.join(self.config.storage, 'metadata')
|
self.metadir = os.path.join(self.config.storage, "metadata")
|
||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
if self.request.headers.get("Content-Type", "").startswith("application/json"):
|
if self.request.headers.get("Content-Type", "").startswith("application/json"):
|
||||||
|
@ -222,7 +298,11 @@ class AnnotationsHandler(tornado.web.RequestHandler):
|
||||||
self.json_args = None
|
self.json_args = None
|
||||||
|
|
||||||
def get_filenames(self):
|
def get_filenames(self):
|
||||||
return [name[:-16] for name in os.listdir(self.config.storage) if name.endswith('json_appendable')]
|
return [
|
||||||
|
name[:-16]
|
||||||
|
for name in os.listdir(self.config.storage)
|
||||||
|
if name.endswith("json_appendable")
|
||||||
|
]
|
||||||
|
|
||||||
def get(self, filename):
|
def get(self, filename):
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
|
@ -233,17 +313,16 @@ class AnnotationsHandler(tornado.web.RequestHandler):
|
||||||
if filename not in filenames:
|
if filename not in filenames:
|
||||||
raise tornado.web.HTTPError(404)
|
raise tornado.web.HTTPError(404)
|
||||||
|
|
||||||
|
meta_file = os.path.join(self.metadir, filename + ".json")
|
||||||
meta_file = os.path.join(self.metadir, filename +'.json')
|
|
||||||
if not os.path.exists(meta_file):
|
if not os.path.exists(meta_file):
|
||||||
self.set_status(404)
|
self.set_status(404)
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(meta_file, 'r') as fp:
|
with open(meta_file, "r") as fp:
|
||||||
self.write(json.load(fp))
|
self.write(json.load(fp))
|
||||||
|
|
||||||
def post(self, filename):
|
def post(self, filename):
|
||||||
# filename = self.get_argument("file", None)
|
# filename = self.argument("file", None)
|
||||||
|
|
||||||
filenames = self.get_filenames()
|
filenames = self.get_filenames()
|
||||||
print(filenames, filename)
|
print(filenames, filename)
|
||||||
|
@ -254,16 +333,26 @@ class AnnotationsHandler(tornado.web.RequestHandler):
|
||||||
if not os.path.exists(self.metadir):
|
if not os.path.exists(self.metadir):
|
||||||
os.mkdir(self.metadir)
|
os.mkdir(self.metadir)
|
||||||
|
|
||||||
meta_file = os.path.join(self.metadir, filename +'.json')
|
meta_file = os.path.join(self.metadir, filename + ".json")
|
||||||
with open(meta_file, 'w') as fp:
|
with open(meta_file, "w") as fp:
|
||||||
json.dump(self.json_args, fp)
|
json.dump(self.json_args, fp)
|
||||||
|
|
||||||
|
class IndexHandler(tornado.web.RequestHandler):
|
||||||
|
"""Get annotation as svg"""
|
||||||
|
|
||||||
|
def initialize(self, config, index: svganim.strokes.AnnotationIndex):
|
||||||
|
self.config = config
|
||||||
|
self.index = index
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
self.render("templates/index.html", index=self.index)
|
||||||
|
|
||||||
class Server:
|
class Server:
|
||||||
"""
|
"""
|
||||||
Server for HIT -> plotter events
|
Server for HIT -> plotter events
|
||||||
As well as for the Status interface
|
As well as for the Status interface
|
||||||
"""
|
"""
|
||||||
|
|
||||||
loop = None
|
loop = None
|
||||||
|
|
||||||
def __init__(self, config, logger):
|
def __init__(self, config, logger):
|
||||||
|
@ -271,33 +360,59 @@ class Server:
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
# self.config['server']['port']
|
# self.config['server']['port']
|
||||||
self.web_root = os.path.join('www')
|
self.web_root = os.path.join("www")
|
||||||
|
|
||||||
|
self.index = svganim.strokes.AnnotationIndex(
|
||||||
|
"annotation_index.shelve", "files", "files/metadata"
|
||||||
|
)
|
||||||
|
self.logger.info("Loading Annotation Index")
|
||||||
|
self.index.refresh()
|
||||||
|
self.logger.info("\tloaded annotation index")
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
application = tornado.web.Application([
|
application = tornado.web.Application(
|
||||||
(r"/ws(.*)", WebSocketHandler, {
|
[
|
||||||
'config': self.config,
|
(
|
||||||
}),
|
r"/ws(.*)",
|
||||||
|
WebSocketHandler,
|
||||||
|
{
|
||||||
|
"config": self.config,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(r"/files/(.*)", AnimationHandler, {"config": self.config}),
|
||||||
|
(
|
||||||
|
r"/audio/(.+)",
|
||||||
|
tornado.web.StaticFileHandler,
|
||||||
|
{"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"/tags/(.+)",
|
||||||
|
TagAnnotationsHandler,
|
||||||
|
{"config": self.config, "index": self.index},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/annotation/(.+)",
|
||||||
|
AnnotationHandler,
|
||||||
|
{"config": self.config, "index": self.index},
|
||||||
|
),
|
||||||
|
(r"/(.+)", StaticFileWithHeaderHandler, {"path": self.web_root}),
|
||||||
|
|
||||||
(r"/files/(.*)", AnimationHandler,
|
(r"/", IndexHandler, {"config": self.config, "index": self.index}),
|
||||||
{'config': self.config}),
|
],
|
||||||
(r"/audio/(.+)", tornado.web.StaticFileHandler,
|
debug=True,
|
||||||
{"path": os.path.join(self.config.storage, 'audio')}),
|
autoreload=True,
|
||||||
(r"/audio", AudioListingHandler,
|
)
|
||||||
{'config': self.config}),
|
|
||||||
(r"/annotations/(.+)", AnnotationsHandler,
|
|
||||||
{'config': self.config}),
|
|
||||||
(r"/(.*)", StaticFileWithHeaderHandler,
|
|
||||||
{"path": self.web_root}),
|
|
||||||
], debug=True, autoreload=True)
|
|
||||||
application.listen(self.config.port)
|
application.listen(self.config.port)
|
||||||
tornado.ioloop.IOLoop.current().start()
|
tornado.ioloop.IOLoop.current().start()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
argParser = argparse.ArgumentParser(
|
argParser = argparse.ArgumentParser(
|
||||||
description='Start up the vector animation server')
|
description="Start up the vector animation server"
|
||||||
|
)
|
||||||
# argParser.add_argument(
|
# argParser.add_argument(
|
||||||
# '--config',
|
# '--config',
|
||||||
# '-c',
|
# '-c',
|
||||||
|
@ -305,50 +420,40 @@ if __name__ == "__main__":
|
||||||
# type=str,
|
# type=str,
|
||||||
# help='The yaml config file to load'
|
# help='The yaml config file to load'
|
||||||
# )
|
# )
|
||||||
|
argParser.add_argument("--port", type=int, default=7890, help="Port")
|
||||||
argParser.add_argument(
|
argParser.add_argument(
|
||||||
'--port',
|
"--storage", type=str, default="files", help="directory name for output files"
|
||||||
type=int,
|
|
||||||
default=7890,
|
|
||||||
help='Port'
|
|
||||||
)
|
|
||||||
argParser.add_argument(
|
|
||||||
'--storage',
|
|
||||||
type=str,
|
|
||||||
default='files',
|
|
||||||
help='directory name for output files'
|
|
||||||
)
|
|
||||||
argParser.add_argument(
|
|
||||||
'--verbose',
|
|
||||||
'-v',
|
|
||||||
action='count', default=0
|
|
||||||
)
|
)
|
||||||
|
argParser.add_argument("--verbose", "-v", action="count", default=0)
|
||||||
|
|
||||||
args = argParser.parse_args()
|
args = argParser.parse_args()
|
||||||
|
|
||||||
loglevel = logging.NOTSET if args.verbose > 1 else logging.DEBUG if args.verbose > 0 else logging.INFO
|
loglevel = (
|
||||||
|
logging.NOTSET
|
||||||
|
if args.verbose > 1
|
||||||
|
else logging.DEBUG
|
||||||
|
if args.verbose > 0
|
||||||
|
else logging.INFO
|
||||||
|
)
|
||||||
|
|
||||||
coloredlogs.install(
|
coloredlogs.install(
|
||||||
level=loglevel,
|
level=loglevel,
|
||||||
# default: "%(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s"
|
# default: "%(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s"
|
||||||
fmt="%(asctime)s %(hostname)s %(name)s[%(process)d,%(threadName)s] %(levelname)s %(message)s"
|
fmt="%(asctime)s %(hostname)s %(name)s[%(process)d,%(threadName)s] %(levelname)s %(message)s",
|
||||||
)
|
)
|
||||||
|
|
||||||
# File logging
|
# File logging
|
||||||
formatter = logging.Formatter(fmt='%(asctime)s %(module)s:%(lineno)d %(levelname)8s | %(message)s',
|
formatter = logging.Formatter(
|
||||||
datefmt='%Y/%m/%d %H:%M:%S') # %I:%M:%S %p AM|PM format
|
fmt="%(asctime)s %(module)s:%(lineno)d %(levelname)8s | %(message)s",
|
||||||
|
datefmt="%Y/%m/%d %H:%M:%S",
|
||||||
|
) # %I:%M:%S %p AM|PM format
|
||||||
logFileHandler = logging.handlers.RotatingFileHandler(
|
logFileHandler = logging.handlers.RotatingFileHandler(
|
||||||
'log/draw_log.log',
|
"log/draw_log.log", maxBytes=1024 * 512, backupCount=5
|
||||||
maxBytes=1024*512,
|
|
||||||
backupCount=5
|
|
||||||
)
|
)
|
||||||
logFileHandler.setFormatter(formatter)
|
logFileHandler.setFormatter(formatter)
|
||||||
|
|
||||||
logger = logging.getLogger("sorteerhoed")
|
logger.addHandler(logFileHandler)
|
||||||
logger.addHandler(
|
|
||||||
logFileHandler
|
|
||||||
)
|
|
||||||
logger.info(f"Start server: http://localhost:{args.port}")
|
logger.info(f"Start server: http://localhost:{args.port}")
|
||||||
|
|
||||||
server = Server(args, logger)
|
server = Server(args, logger)
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue