Compare commits

...

4 commits

Author SHA1 Message Date
Ruben van de Ven
8c850d22b4 Add parse_offsets utility script 2022-09-12 11:20:07 +02:00
Ruben van de Ven
6c3f960ffb Expand Readme and add LICENSE file 2022-09-12 11:19:45 +02:00
Ruben van de Ven
d68450361e PNG output option for images 2022-06-28 17:10:15 +02:00
Ruben van de Ven
02dd1a69d0 New tags version 2022-06-28 17:09:16 +02:00
7 changed files with 594 additions and 33 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Ruben van de Ven
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

116
README.md
View file

@ -1,15 +1,113 @@
# svganim
# [svganim] Drawing & annotation
Create a hand drawn vector animation.
This software allows for drawings to be made and played back in a time-based format. Additionally, it contains tools to create excerpts of the drawings and browse these segments by annotation.
```bash
poetry run python webserver.py
```
This software was developed in light of a research project in which we used it in an interview setting. Asking speakers not just to speak, but to draw the entities they mention and the relations between them. This adds a visual layer on top of the audio.
`parse_offsets.py` can be used to pad the diagram in order to sync it with the audio. This is necessary eg. after a network failure. It works by adding a line with the required offset to the `.json_appendable`-file.
For more information on the rationale behind developing this tool, see the [citation](#citation) section.
## record
<p align="center">
<a href="#getting-started">Getting Started</a>
<a href="#usage">Usage</a>
<a href="#license">License</a>
<a href="#credits">Credits</a>
<a href="#citation">Citation</a>
</p>
On Linux: for now, set the pulse default input device to the monitor of the speakers. Then use wf-recorder:
`wf-recorder -g"$(slurp)" -a -f recording.mp4`
## Getting Started
These instructions will give you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on deploying the project on a live system.
### Prerequisites
The easiest way to run svganim is with docker-compose. Alternatively you can choose set up your own python environment. If you do so, ffmpeg is required.
### Installation
* `git clone` this repository.
* `cd` into the folder and run `docker-compose build`
* `docker-compose up -d`
* Docker should now expose the service at port 7890. Edit docker-compose.yml to change this.
### Update
* `git pull` the new revision
* rerun `docker-compose build` to rebuild the image
* rerun `docker-compose up -d` to restart the container
### Deployment
The server is best exposed through a proxy. An [example Apache configuration file](./diagramming-apache-site.example.conf) is included.
## Usage
When the service runs, open it using your browser. The first page shows three options:
* Tags -- Manage a set of tags for annotating.
* Drawings -- All recorded drawings to be played back and annotated (or removed).
* Draw -- start drawing a diagram.
### Drawing
* By default, the drawing page gives four colour options. Select the desired colour on the left.
* The big 'Fullscreen' button puts the canvas in fullscreen. To exit, use the escape key.
* The drawing will be recorded from the moment the pen touches the canvas.
* When you stop drawing (ie. close the tab or browser) you can recover the drawing by going to 'Drawings' and select 'Draw' at the drawing you want to continue.
* **NOTE** Please note that when continuing a drawing, the intermediate time _is not captured_. Ie. the last stroke of the first drawing, will be immediately followed by the first stroke from the second drawing.
* A pen display such as a Wacom or Huion is recommended but not at all required.
### Annotating
* Before annotating, make sure to configure your initial tags on the tags page (`URL/index`)
* [optional] Add audio
* add an audio recording of the conversation to `files/audio`. Note that a wav file can be quite slow as everything goes through a browser. So possibly convert wav to mp3 or ogg.
* Open the drawing in the interface URL/annotate.html. Hover the title on the top left, and select the audio file from the pull down.
* Use the _Offset_ parameter to align the audio to the drawing (number in seconds).
* _NOTE_: The diagram starts recoding on the first stroke. Often the audio file starts before the drawing. This requires a _negative_ offset.
* The title of the drawing can be set and changed by clicking on it.
* To annotate, select an in and out point using the bar below the drawing.
* Alternatively, in and out point can be set to the playhead `i` and `o` keys.
* When the in or out point is selected, it can be moved wit the arrow keys, or page up/down for larger increments.
* To precisely enter a timecode, click the time label of the point to type it.
* Then click the desired tag from the overview on the left.
* Creating an annotation, immediately deselects it, and sets the selected area to the time after annotation.
* Click an annotation to select it
* Use the textbox at the bottom to add a comment. This will be visible in the tags index page.
* After selecting an annotation, click it again to deselect. Alternatively, use the `esc` key.
* Deselecting an annotation sets the selected area to the time after it.
### Tags
With the tags interface you can manage a nested set of tags and manage the annotated segments. In our workflow, we had annotated the interviews, but tweaked the terms to create interesting subsets of drawings.
* To create a nested tag, select a tag, and an 'add tag' option will appear.
* Double-click the title to rename.
* Use the bucket icon to remove a tag.
* Click the coloured square to tweak the colour.
* In the overview of excerpts, use the checkboxes to change the tags in batch.
* Clicking an excerpt plays it back.
### Export as video?
There is no integrated a way to export the excerpts to a video file. The easiest way to do so however is to make a screen capture of a part of your display.
E.g. on Linux: set the pulse default input device to the monitor of the speakers. Then use wf-recorder: `wf-recorder -g"$(slurp)" -a -f recording.mp4`
### Notes
The `parse_offsets.py` script can be used to pad the diagram in order to sync it with the audio. This is necessary eg. after a network failure. It works by adding a line with the required offset to the `.json_appendable`-file. It dumps the output to the terminal. So use it with a redirect. E.g. `python parse_offsets.py -i faulty_file.json_appendable > new_file.json_appendable`.
## License
This code is made available under the MIT license. For details see [LICENSE](./LICENSE)
## Credits
This software uses (and for ease of use, redistributes) the [noUiSlider](https://refreshless.com/nouislider/) and [wnumb](https://refreshless.com/wnumb/) javascript libraries, developed by [Léon Gersen](https://refreshless.com/). Also using the MIT License.
## Citation
van de Ven, Ruben, and Ildikó Zonga Plájás 2022. _Inconsistent Projections: Con-figuring Security Vision Through Diagramming_ [Manuscript submitted for publication].

45
app/parse_offsets.py Normal file
View file

@ -0,0 +1,45 @@
import json
import logging
import argparse
import sys
logger = logging.getLogger("svganim.fixer")
argParser = argparse.ArgumentParser(
description="Helper tool to fix timings after glitch"
)
argParser.add_argument(
"--input", type=str, help="Filename to run on (.json_appendable)"
)
args = argParser.parse_args()
with open(args.input, "r") as fp:
events = json.loads("[" + fp.read() + "]")
time_offset = 0
for i, event in enumerate(events):
if type(event) is list:
pass
elif event['event'] == "offset":
time_offset += event['offset']
# print a reverseable event that is ignored by this script
event = {
'event': 'added_offset',
'offset': event['offset']
}
elif event['event'] == 'added_offset':
logger.warning('ignore existing offset')
continue
elif event["event"] == "viewbox":
for key, box in enumerate(event['viewboxes']):
event['viewboxes'][key]['t'] += time_offset
elif event["event"] == "stroke":
event['points'] = [[p[0], p[1], p[2], p[3] + time_offset]
for p in event['points']]
if i>0:
sys.stdout.write(",\n")
sys.stdout.write(json.dumps(event))
if time_offset == 0:
logger.warning('\n\nNo offset defined in file, please do so by adding {"event":"offset", "offset": miliseconds} at the line from which the offset should be added.')

View file

@ -16,6 +16,7 @@ import glob
import filelock
import svganim.strokes
import svganim.uimethods
import cairosvg
logger = logging.getLogger("svganim.webserver")
@ -288,6 +289,9 @@ class AnimationHandler(tornado.web.RequestHandler):
if filename[-4:] == ".svg":
extension = "svg"
filename = filename[:-4]
elif filename[-4:] == ".png":
extension = "png"
filename = filename[:-4]
elif filename[-4:] == ".mp3":
extension = "mp3"
filename = filename[:-4]
@ -316,6 +320,10 @@ class AnimationHandler(tornado.web.RequestHandler):
if extension == "svg":
self.set_header("Content-Type", "image/svg+xml")
self.write(animation.get_as_svg())
elif extension == "png":
self.set_header("Content-Type", "image/png")
svgstring = animation.get_as_svg()
self.write(cairosvg.svg2png(bytestring=svgstring))
elif extension == "mp3":
self.set_header("Content-Type", "audio/mp3")
audio = await animation.audio.export(format="mp3")
@ -381,6 +389,9 @@ class AnnotationHandler(tornado.web.RequestHandler):
if annotation_id[-4:] == ".svg":
extension = "svg"
annotation_id = annotation_id[:-4]
elif annotation_id[-4:] == ".png":
extension = "png"
annotation_id = annotation_id[:-4]
elif annotation_id[-4:] == ".mp3":
extension = "mp3"
annotation_id = annotation_id[:-4]
@ -401,6 +412,10 @@ class AnnotationHandler(tornado.web.RequestHandler):
self.set_header("Cache-Control", "max-age=31536000, immutable")
self.write(annotation.get_as_svg())
elif extension == "png":
self.set_header("Content-Type", "image/png")
svgstring = annotation.get_as_svg()
self.write(cairosvg.svg2png(bytestring=svgstring))
elif extension == "mp3":
self.set_header("Content-Type", "audio/mp3")
self.write(annotation.getAnimationSlice(
@ -465,6 +480,9 @@ class DrawingHandler(tornado.web.RequestHandler):
if drawing_id[-4:] == ".svg":
extension = "svg"
drawing_id = drawing_id[:-4]
elif drawing_id[-4:] == ".png":
extension = "png"
drawing_id = drawing_id[:-4]
elif drawing_id[-4:] == ".mp3":
extension = "mp3"
drawing_id = drawing_id[:-4]
@ -486,6 +504,10 @@ class DrawingHandler(tornado.web.RequestHandler):
if extension == "svg":
self.set_header("Content-Type", "image/svg+xml")
self.write(drawing.get_animation().get_as_svg())
if extension == "png":
self.set_header("Content-Type", "image/png")
svgstring =drawing.get_animation().get_as_svg()
self.write(cairosvg.svg2png(bytestring = svgstring))
elif extension == "mp3":
self.set_header("Content-Type", "audio/mp3")
self.write(drawing.get_animation(

View file

@ -3,36 +3,42 @@
"name": "root",
"color": null,
"description": "",
"annotation_count": 0,
"children": [
{
"id": "human-machine",
"name": "Human/machine Entanglements (Appearance/disappearance)",
"color": "#ffa348",
"description": "",
"annotation_count": 2,
"children": [
{
"id": "vision",
"name": "Vision",
"color": null,
"description": ""
"description": "",
"annotation_count": 1
},
{
"id": "sound",
"name": "sound",
"color": null,
"description": ""
"description": "",
"annotation_count": 0
},
{
"id": "behaviour",
"name": "behaviour",
"color": null,
"description": ""
"description": "",
"annotation_count": 1
},
{
"id": "other-senses",
"name": "Other senses",
"color": null,
"description": ""
"description": "",
"annotation_count": 0
}
]
},
@ -40,25 +46,29 @@
"id": "tensions",
"name": "Tensions, contestations & problems",
"color": "#77767b",
"description": "Which problems are identified?, when do they become problems?"
"description": "Which problems are identified?, when do they become problems?",
"annotation_count": 13
},
{
"id": "security",
"name": "Security & types of data",
"color": "#3584e4",
"description": "",
"annotation_count": 3,
"children": [
{
"id": "definitions",
"name": "definitions",
"color": null,
"description": "e.g. domain knowledge"
"description": "e.g. domain knowledge",
"annotation_count": 12
},
{
"id": "input",
"name": "input",
"color": null,
"description": "e.g. fake data"
"description": "e.g. fake data",
"annotation_count": 2
}
]
},
@ -67,54 +77,63 @@
"name": "Actants in relation",
"color": "#fa08ff",
"description": "",
"annotation_count": 5,
"children": [
{
"id": "algorithm",
"name": "algorithm",
"color": null,
"description": ""
"description": "",
"annotation_count": 2
},
{
"id": "technologies",
"name": "technologies",
"color": null,
"description": ""
"description": "",
"annotation_count": 4
},
{
"id": "frt",
"name": "frt",
"color": null,
"description": ""
"description": "",
"annotation_count": 4
},
{
"id": "cameras",
"name": "CCTV & camera's",
"color": null,
"description": ""
"description": "",
"annotation_count": 5
},
{
"id": "entities",
"name": "Entities: people, institutions etc.",
"color": null,
"description": ""
"description": "",
"annotation_count": 9
},
{
"id": "positioning",
"name": "Positioning",
"color": null,
"description": "the positioning of a field/person/oneself in relation to others"
"description": "the positioning of a field/person/oneself in relation to others",
"annotation_count": 9
},
{
"id": "inside-outside",
"name": "inside-outside",
"color": null,
"description": ""
"description": "",
"annotation_count": 0
},
{
"id": "public-private",
"name": "public-private",
"color": null,
"description": ""
"description": "",
"annotation_count": 3
}
]
},
@ -123,30 +142,35 @@
"name": "consequences",
"color": "#0add32",
"description": "",
"annotation_count": 2,
"children": [
{
"id": "effects",
"name": "effects",
"color": null,
"description": ""
"description": "",
"annotation_count": 3
},
{
"id": "future-imaginaries",
"name": "future-imaginaries",
"color": null,
"description": ""
"description": "",
"annotation_count": 2
},
{
"id": "speculations",
"name": "speculations",
"color": null,
"description": "what is & what will/can be done."
"description": "what is & what will/can be done.",
"annotation_count": 6
},
{
"id": "innovations",
"name": "innovations",
"color": null,
"description": ""
"description": "",
"annotation_count": 2
}
]
},
@ -154,19 +178,137 @@
"id": "hesitation",
"name": "Hesitations & corrections",
"color": "#f8e45c",
"description": ""
"description": "",
"annotation_count": 10
},
{
"id": "skip",
"name": "skip",
"color": null,
"description": ""
"description": "",
"annotation_count": 16
},
{
"id": "todo",
"name": "to do / interesting",
"color": "#ff0000",
"description": ""
"description": "",
"annotation_count": 7
},
{
"id": "6f9a83a1-e374-4dc9-a6c4-39c5461bb435",
"name": "Threat & Risk - What is being protected by SV",
"color": "#813d9c",
"description": "",
"annotation_count": 0
},
{
"id": "3fda0504-00ff-485e-9faf-ca7d08e4e96c",
"name": "Errors & Glitches (having it wrong)",
"color": "#986a44",
"description": "",
"annotation_count": 0,
"children": [
{
"id": "0f758538-ea27-424f-bc0a-d6a68837832d",
"name": "FPR",
"color": null,
"description": "",
"annotation_count": 0
},
{
"id": "6f10e893-2fcf-4c72-9480-04cad5612cc2",
"name": "Consequences of an error",
"color": null,
"description": "",
"annotation_count": 0
}
]
},
{
"id": "63bd867b-d7d4-4247-b6a6-9157ab24cff8",
"name": "Sites of SV",
"color": "#99c1f1",
"description": "",
"annotation_count": 0,
"children": [
{
"id": "0fd34742-7582-42ec-96a5-ee83ad57fbb2",
"name": "Public space",
"color": null,
"description": "",
"annotation_count": 0
}
]
},
{
"id": "bd8ab328-85e7-44d2-bfee-2ad951587116",
"name": "Temporality",
"color": "#ffff00",
"description": "",
"annotation_count": 0,
"children": [
{
"id": "a20fb815-db9a-4704-8ac1-92a5f8a5497b",
"name": "Static images",
"color": null,
"description": "",
"annotation_count": 0
},
{
"id": "ac8e247a-1ed2-452c-af6f-2b345706e3eb",
"name": "Technicalities of time",
"color": null,
"description": "",
"annotation_count": 0
}
]
},
{
"id": "39f4a6fe-9c15-4417-bce7-8be9ef791bf2",
"name": "Humans & Bodies",
"color": "#dc8add",
"description": "",
"annotation_count": 0,
"children": [
{
"id": "b38c9a6a-837f-4301-9f82-69d9c22438c9",
"name": "Absences of bodies",
"color": null,
"description": "",
"annotation_count": 0
},
{
"id": "89a1924c-ec49-460e-9b79-789237b23429",
"name": "Human-in-the-loop",
"color": null,
"description": "",
"annotation_count": 0
},
{
"id": "e906230f-332e-4bd4-8939-936baa05acc5",
"name": "The human as a crowd",
"color": null,
"description": "",
"annotation_count": 0
}
]
},
{
"id": "0a37911b-a458-491c-9145-534121d5fc92",
"name": "Solutions",
"color": "#f6f5f4",
"description": "",
"annotation_count": 0,
"children": [
{
"id": "dce213cb-1fc6-4a20-9cb2-82862119fa5d",
"name": "Juridical/Law",
"color": null,
"description": "",
"annotation_count": 0
}
]
}
]
}

236
poetry.lock generated
View file

@ -13,6 +13,52 @@ six = ">=1.9.0"
dev = ["check-manifest"]
test = ["coverage"]
[[package]]
name = "cairocffi"
version = "1.3.0"
description = "cffi-based cairo bindings for Python"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
cffi = ">=1.1.0"
[package.extras]
doc = ["sphinx", "sphinx-rtd-theme"]
test = ["pytest-runner", "pytest-cov", "pytest-flake8", "pytest-isort"]
xcb = ["xcffib (>=0.3.2)"]
[[package]]
name = "cairosvg"
version = "2.5.2"
description = "A Simple SVG Converter based on Cairo"
category = "main"
optional = false
python-versions = ">=3.5"
[package.dependencies]
cairocffi = "*"
cssselect2 = "*"
defusedxml = "*"
pillow = "*"
tinycss2 = "*"
[package.extras]
doc = ["sphinx", "sphinx-rtd-theme"]
test = ["pytest-runner", "pytest-cov", "pytest-flake8", "pytest-isort"]
[[package]]
name = "cffi"
version = "1.15.0"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pycparser = "*"
[[package]]
name = "coloredlogs"
version = "15.0.1"
@ -27,11 +73,35 @@ humanfriendly = ">=9.1"
[package.extras]
cron = ["capturer (>=2.4)"]
[[package]]
name = "cssselect2"
version = "0.6.0"
description = "CSS selectors for Python ElementTree"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
tinycss2 = "*"
webencodings = "*"
[package.extras]
doc = ["sphinx", "sphinx-rtd-theme"]
test = ["pytest", "pytest-cov", "pytest-flake8", "pytest-isort", "coverage"]
[[package]]
name = "defusedxml"
version = "0.7.1"
description = "XML bomb protection for Python stdlib modules"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "filelock"
version = "3.7.1"
description = "A platform independent file lock."
category = "main"
category = "dev"
optional = false
python-versions = ">=3.7"
@ -50,6 +120,26 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""}
[[package]]
name = "pillow"
version = "9.1.1"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
[[package]]
name = "pycparser"
version = "2.21"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pydub"
version = "0.25.1"
@ -82,6 +172,21 @@ category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "tinycss2"
version = "1.1.1"
description = "A tiny CSS parser"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
webencodings = ">=0.4"
[package.extras]
doc = ["sphinx", "sphinx-rtd-theme"]
test = ["pytest", "pytest-cov", "pytest-flake8", "pytest-isort", "coverage"]
[[package]]
name = "tornado"
version = "6.1"
@ -90,20 +195,95 @@ category = "dev"
optional = false
python-versions = ">= 3.5"
[[package]]
name = "webencodings"
version = "0.5.1"
description = "Character encoding aliases for legacy web content"
category = "main"
optional = false
python-versions = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "11298c8670e03d4235a76555b1ca7e6cbf0740041f14c14fdf409f633e197bf5"
content-hash = "2d0f6605799313075026037a1a147a31ffcb2098bf686d59b85f0d1ca5108ad2"
[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"},
]
cairocffi = [
{file = "cairocffi-1.3.0.tar.gz", hash = "sha256:108a3a7cb09e203bdd8501d9baad91d786d204561bd71e9364e8b34897c47b91"},
]
cairosvg = [
{file = "CairoSVG-2.5.2-py3-none-any.whl", hash = "sha256:98c276b7e4f0caf01e5c7176765c104ffa1aa1461d63b2053b04ab663cf7052b"},
{file = "CairoSVG-2.5.2.tar.gz", hash = "sha256:b0b9929cf5dba005178d746a8036fcf0025550f498ca54db61873322384783bc"},
]
cffi = [
{file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
{file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
{file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
{file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
{file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
{file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
{file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
{file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
{file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
{file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
{file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
{file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
{file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
{file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
{file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
{file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
{file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
{file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
{file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
]
coloredlogs = [
{file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"},
{file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"},
]
cssselect2 = [
{file = "cssselect2-0.6.0-py3-none-any.whl", hash = "sha256:3a83b2a68370c69c9cd3fcb88bbfaebe9d22edeef2c22d1ff3e1ed9c7fa45ed8"},
{file = "cssselect2-0.6.0.tar.gz", hash = "sha256:5b5d6dea81a5eb0c9ca39f116c8578dd413778060c94c1f51196371618909325"},
]
defusedxml = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
filelock = [
{file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
{file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
@ -112,6 +292,50 @@ humanfriendly = [
{file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"},
{file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"},
]
pillow = [
{file = "Pillow-9.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe"},
{file = "Pillow-9.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e"},
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602"},
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530"},
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2"},
{file = "Pillow-9.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a"},
{file = "Pillow-9.1.1-cp310-cp310-win32.whl", hash = "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c"},
{file = "Pillow-9.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108"},
{file = "Pillow-9.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b"},
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d"},
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84"},
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4"},
{file = "Pillow-9.1.1-cp37-cp37m-win32.whl", hash = "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578"},
{file = "Pillow-9.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9"},
{file = "Pillow-9.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf"},
{file = "Pillow-9.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b"},
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546"},
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f"},
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a"},
{file = "Pillow-9.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1"},
{file = "Pillow-9.1.1-cp38-cp38-win32.whl", hash = "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54"},
{file = "Pillow-9.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf"},
{file = "Pillow-9.1.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92"},
{file = "Pillow-9.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a"},
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251"},
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d"},
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1"},
{file = "Pillow-9.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601"},
{file = "Pillow-9.1.1-cp39-cp39-win32.whl", hash = "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45"},
{file = "Pillow-9.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c"},
{file = "Pillow-9.1.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6"},
{file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098"},
{file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8"},
{file = "Pillow-9.1.1.tar.gz", hash = "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0"},
]
pycparser = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
pydub = [
{file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"},
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
@ -128,6 +352,10 @@ svgwrite = [
{file = "svgwrite-1.4.2-py3-none-any.whl", hash = "sha256:ca63d76396d1f6f099a2b2d8cf1419e1c1de8deece9a2b7f4da0632067d71d43"},
{file = "svgwrite-1.4.2.zip", hash = "sha256:d304a929f197d31647c287c10eee0f64378058e1c83a9df83433a5980864e59f"},
]
tinycss2 = [
{file = "tinycss2-1.1.1-py3-none-any.whl", hash = "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8"},
{file = "tinycss2-1.1.1.tar.gz", hash = "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf"},
]
tornado = [
{file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"},
{file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"},
@ -171,3 +399,7 @@ tornado = [
{file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"},
{file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
]
webencodings = [
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
]

View file

@ -12,6 +12,7 @@ pydub = "^0.25.1"
svgwrite = "^1.4.1"
anytree = "^2.8.0"
filelock = "^3.7.1"
CairoSVG = "^2.5.2"
[tool.poetry.dev-dependencies]