Compare commits

..

2 commits

Author SHA1 Message Date
Ruben van de Ven
2c5d105e7f Drawing extraction and some fixes 2022-01-19 10:28:51 +01:00
Ruben van de Ven
d04ccd3c5b Fix js loading/playback issues 2022-01-11 12:00:28 +01:00
5 changed files with 386 additions and 175 deletions

View file

@ -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
View 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

View file

@ -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

View file

@ -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()

View file

@ -311,7 +311,9 @@ class Annotator extends EventTarget {
fetch(metadata_req) fetch(metadata_req)
.then(response => response.ok ? response.json() : null) .then(response => response.ok ? response.json() : null)
.then(metadata => { .then(metadata => {
if (metadata !== null) {
metadata.annotations = metadata.annotations.map((a) => new Annotation(a.tag, a.t_in, a.t_out)) metadata.annotations = metadata.annotations.map((a) => new Annotation(a.tag, a.t_in, a.t_out))
}
this.loadStrokes(data, metadata) this.loadStrokes(data, metadata)
}) })
.catch(e => console.log(e)); .catch(e => console.log(e));
@ -754,7 +756,9 @@ class Annotator extends EventTarget {
} }
// when an outpoint is set, stop playing there // when an outpoint is set, stop playing there
if (this.outPointPosition && (next_path > this.outPointPosition[0] || next_point > this.outPointPosition[1])) { if (this.outPointPosition && (next_path > this.outPointPosition[0] ||
(next_path == this.outPointPosition[0] && next_point > this.outPointPosition[1]))) {
console.log('> out point', this.outPointPosition)
return [null, null]; return [null, null];
} }
@ -945,7 +949,8 @@ class Annotator extends EventTarget {
getEndTimeMs() { getEndTimeMs() {
const videoDuration = this.getFinalFrameTime(); const videoDuration = this.getFinalFrameTime();
const audioDuration = (this.audioEl) ? this.audioEl.duration + this.audioOffset : 0; const audioDuration = (this.audioEl && this.audioEl.src) ? this.audioEl.duration + this.audioOffset : 0;
return Math.max(videoDuration, audioDuration * 1000); return Math.max(videoDuration, audioDuration * 1000);
} }