.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GENEA 2020 BVH Visualizer
2 |
3 |
4 |
5 | Example output from the visualization server
6 |
7 |
8 |
9 | This repository provides scripts that can be used to visualize BVH files. These scripts were developed for the [GENEA Challenge 2020](https://genea-workshop.github.io/2020/#gesture-generation-challenge), and enables reproducing the visualizations used for the challenge stimuli.
10 | The server consists of several containers which are launched together with the docker-compose command described below.
11 | The components are:
12 | * web: this is the HTTP server which receives render requests and places them on a "celery" queue to be processed.
13 | * worker: this takes jobs from the "celery" queue and works on them. Each worker runs one Blender process, so increasing the amount of workers adds more parallelization.
14 | * monitor: this is a monitoring tool for celery. Default username is `user` and password is `password` (can be changed by setting `FLOWER_USER` and `FLOWER_PWD` when starting the docker-compose command)
15 | * redis: needed for celery
16 |
17 | ## GENEA Challenge 2022 BVH Visualizer
18 | A newer version of the visualizer used for the GENEA Challenge 2022 can be found in [this fork](https://github.com/TeoNikolov/genea_visualizer)
19 |
20 |
21 | ## Build and start visualization server
22 | First you need to install docker-compose:
23 | `sudo apt install docker-compose` (on Ubuntu)
24 |
25 | You might want to edit some of the default parameters, such as render resolution and fps, in the `.env` file.
26 |
27 | Then to start the server run `docker-compose up --build`
28 |
29 | In order to run several (for example 3) workers (Blender renderers, which allows to parallelize rendering, run `docker-compose up --build --scale worker=3`
30 |
31 | The `-d` flag can also be passed in order to run the server in the background. Logs can then be accessed by running `docker-compose logs -f`. Additionally it's possible to rebuild just the worker or API containers with minimal disruption in the running server by running for example `docker-compose up -d --no-deps --scale worker=2 --build worker`. This will rebuild the worker container and stop the old ones and start 2 new ones.
32 |
33 | ## Use the visualization server
34 | The server is HTTP-based and works by uploading a bvh file. You will then receive a "job id" which you can poll in order to see the progress of your rendering. When it is finished you will receive a URL to a video file that you can download.
35 | Below are some examples using `curl` and in the file `example.py` there is a full python (3.7) example of how this can be used.
36 |
37 | Since the server is available publicly online, a simple authentication system is included – just pass in the token `j7HgTkwt24yKWfHPpFG3eoydJK6syAsz` with each request. This can be changed by modifying `USER_TOKEN` in `.env`.
38 |
39 | For a simple usage example, you can see a full python script in `example.py`.
40 |
41 | Otherwise, you can follow the detailed instructions on how to use the visualization server provided below.
42 |
43 | Depending on where you host the visualization, `SERVER_URL` might be different. If you just are running it locally on your machine you can use `127.0.0.1` but otherwise you would use the ip address to the machine that is hosting the server.
44 |
45 | ```curl -XPOST -H "Authorization:Bearer j7HgTkwt24yKWfHPpFG3eoydJK6syAsz" -F "file=@/path/to/bvh/file.bvh" http://SERVER_URL/render```
46 | will return a URI to the current job `/jobid/[JOB_ID]`.
47 |
48 | `curl -H "Authorization:Bearer j7HgTkwt24yKWfHPpFG3eoydJK6syAsz" http://SERVER_URL/jobid/[JOB_ID]` will return the current job state, which might be any of:
49 | * `{result": {"jobs_in_queue": X}, "state": "PENDING"}`: Which means the job is in the queue and waiting to be rendered. The `jobs_in_queue` property is the total number of jobs waiting to be executed. The order of job execution is not guaranteed, which means that this number does not reflect how many jobs there are before the current job, but rather reflects if the server is currently busy or not.
50 | * `{result": null, "state": "PROCESSING"}`: The job is currently being processed. Depending on the file size this might take a while, but this acknowledges that the server has started to working on the request.
51 | * `{result":{"current": X, "total": Y}, "state": "RENDERING"}`: The job is currently being rendered, this is the last stage of the process. `current` shows which is the last rendered frame and `total` shows how many frames in total this job will render.
52 | * `{"result": FILE_URL, "state": "SUCCESS"}`: The job ended successfully and the video is available at `http://SERVER_URL/[FILE_URL]`.
53 | * `{"result": ERROR_MSG, "state": "FAILURE"}`: The job ended with a failure and the error message is given in `results`.
54 |
55 | In order to retrieve the video, run `curl -H "Authorization:Bearer j7HgTkwt24yKWfHPpFG3eoydJK6syAsz" http://SERVER_URL/[FILE_URL] -o result.mp4`. Please note that the server will delete the file after you retrieve it, so you can only retrieve it once!
56 |
57 | ## Replicating the GENEA Challenge 2020 visualizations
58 | The parameters in the enclosed file `docker-compose-genea.yml` correspond to those that were used to render the final evaluation stimuli of the GENEA Challenge, for ease of replication.
59 |
60 | ### If you use this code in your research please cite our IUI article:
61 | ```
62 | @inproceedings{kucherenko2021large,
63 | author = {Kucherenko, Taras and Jonell, Patrik and Yoon, Youngwoo and Wolfert, Pieter and Henter, Gustav Eje},
64 | title = {A Large, Crowdsourced Evaluation of Gesture Generation Systems on Common Data: {T}he {GENEA} {C}hallenge 2020},
65 | year = {2021},
66 | isbn = {9781450380171},
67 | publisher = {Association for Computing Machinery},
68 | address = {New York, NY, USA},
69 | url = {https://doi.org/10.1145/3397481.3450692},
70 | doi = {10.1145/3397481.3450692},
71 | booktitle = {26th International Conference on Intelligent User Interfaces},
72 | pages = {11--21},
73 | numpages = {11},
74 | keywords = {evaluation paradigms, conversational agents, gesture generation},
75 | location = {College Station, TX, USA},
76 | series = {IUI '21}
77 | }
78 | ```
79 |
--------------------------------------------------------------------------------
/api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8-alpine
2 |
3 | ENV C_FORCE_ROOT true
4 |
5 | # Prompt non-interactive
6 | ENV DEBIAN_FRONTEND=noninteractive
7 |
8 | RUN apk add build-base
9 | RUN pip install uvicorn==0.11.5 uvloop==0.14.0
10 |
11 | COPY . /api
12 | WORKDIR /api
13 |
14 | # install requirements
15 | RUN pip install -r requirements.txt
16 |
17 | # run the app server
18 | CMD uvicorn --host 0.0.0.0 --port $INTERNAL_API_PORT --workers 4 app:app
--------------------------------------------------------------------------------
/api/app.py:
--------------------------------------------------------------------------------
1 | # Copyright 2020 by Patrik Jonell.
2 | # All rights reserved.
3 | # This file is part of the GENEA visualizer,
4 | # and is released under the GPLv3 License. Please see the LICENSE
5 | # file that should have been included as part of this package.
6 |
7 |
8 | import os
9 | from datetime import datetime
10 | from pathlib import Path
11 | from uuid import uuid4
12 |
13 | import celery.states as states
14 | from celery import Celery
15 | from fastapi import BackgroundTasks, FastAPI, File, Request, UploadFile
16 | from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse
17 |
18 | UPLOAD_FOLDER = Path("/tmp/genea_visualizer")
19 | UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
20 |
21 |
22 | celery_workers = Celery(
23 | "tasks",
24 | broker=os.environ["CELERY_BROKER_URL"],
25 | backend=os.environ["CELERY_RESULT_BACKEND"],
26 | )
27 |
28 | app = FastAPI()
29 |
30 |
31 | async def save_tmp_file(upload_file) -> str:
32 | _, extension = os.path.splitext(upload_file.filename)
33 | filename = f"{uuid4()}{extension}"
34 | (UPLOAD_FOLDER / filename).write_bytes(upload_file.file.read())
35 | return f"/files/{filename}"
36 |
37 |
38 | def verify_token(headers, path):
39 | token = headers.get("authorization", "")[7:]
40 | if os.environ["SYSTEM_TOKEN"] == token:
41 | return True
42 | elif not path.startswith("/upload_video") and os.environ["USER_TOKEN"] == token:
43 | return True
44 | else:
45 | return False
46 |
47 |
48 | async def delete_tmp_file(file: Path):
49 | file.unlink()
50 |
51 |
52 | async def remove_old_tmp_files():
53 | for file in UPLOAD_FOLDER.glob("*"):
54 | time_delta = datetime.now() - datetime.fromtimestamp(os.path.getmtime(file))
55 | if time_delta.days > 0:
56 | file.unlink()
57 |
58 |
59 | @app.middleware("http")
60 | async def authorize(request: Request, call_next):
61 | if not verify_token(request.headers, request.scope["path"]):
62 | return JSONResponse(status_code=401)
63 | return await call_next(request)
64 |
65 |
66 | @app.post("/render", response_class=PlainTextResponse)
67 | async def render(background_tasks: BackgroundTasks, file: UploadFile = File(...)):
68 | bvh_file_uri = await save_tmp_file(file)
69 | task = celery_workers.send_task("tasks.render", args=[bvh_file_uri], kwargs={})
70 | background_tasks.add_task(remove_old_tmp_files)
71 | return f"/jobid/{task.id}"
72 |
73 |
74 | @app.get("/jobid/{task_id}")
75 | def check_job(task_id: str) -> str:
76 | res = celery_workers.AsyncResult(task_id)
77 | if res.state == states.PENDING:
78 | reserved_tasks = celery_workers.control.inspect().reserved()
79 | tasks = []
80 | if reserved_tasks:
81 | tasks_per_worker = reserved_tasks.values()
82 | tasks = [item for sublist in tasks_per_worker for item in sublist]
83 | found = False
84 | for task in tasks:
85 | if task["id"] == task_id:
86 | found = True
87 | result = {"jobs_in_queue": len(tasks)}
88 | elif res.state == states.FAILURE:
89 | result = str(res.result)
90 | else:
91 | result = res.result
92 | return {"state": res.state, "result": result}
93 |
94 |
95 | @app.get("/files/{file_name}")
96 | async def files(file_name, background_tasks: BackgroundTasks):
97 | file = UPLOAD_FOLDER / file_name
98 | background_tasks.add_task(delete_tmp_file, file)
99 | return FileResponse(str(file))
100 |
101 |
102 | @app.post("/upload_video", response_class=PlainTextResponse)
103 | async def upload_video(file: UploadFile = File(...)) -> str:
104 | return await save_tmp_file(file)
105 |
--------------------------------------------------------------------------------
/api/requirements.txt:
--------------------------------------------------------------------------------
1 | aiofiles==0.5.0
2 | amqp==2.6.0
3 | billiard==3.6.3.0
4 | celery==4.4.6
5 | click==7.1.2
6 | dataclasses==0.6
7 | fastapi==0.65.2
8 | future==0.18.2
9 | h11==0.9.0
10 | httptools==0.1.1
11 | importlib-metadata==1.6.1
12 | kombu==4.6.11
13 | pydantic==1.6.2
14 | python-multipart==0.0.5
15 | pytz==2020.1
16 | redis==3.5.3
17 | six==1.15.0
18 | starlette==0.14.2
19 | vine==1.3.0
20 | websockets==9.1
21 | zipp==3.1.0
--------------------------------------------------------------------------------
/celery-queue/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:18.04
2 |
3 |
4 | ENV C_FORCE_ROOT true
5 | ENV DEBIAN_FRONTEND=noninteractive
6 |
7 | RUN apt-get update
8 | RUN apt-get -y install python3-pip wget ffmpeg xvfb python-opengl
9 | RUN mkdir /blender && cd /blender && wget -q https://mirror.clarkson.edu/blender/release/Blender2.83/blender-2.83.0-linux64.tar.xz && tar xf /blender/blender-2.83.0-linux64.tar.xz && rm -r /blender/blender-2.83.0-linux64.tar.xz
10 |
11 | COPY . /queue
12 | WORKDIR /queue
13 |
14 | RUN pip3 install -r requirements.txt
15 |
16 | ENTRYPOINT celery -A tasks worker --concurrency 1 --loglevel=info
--------------------------------------------------------------------------------
/celery-queue/blender_render.py:
--------------------------------------------------------------------------------
1 | # Copyright 2020 by Patrik Jonell.
2 | # All rights reserved.
3 | # This file is part of the GENEA visualizer,
4 | # and is released under the GPLv3 License. Please see the LICENSE
5 | # file that should have been included as part of this package.
6 |
7 |
8 | import bpy
9 | from bpy import context
10 | import os
11 | import time
12 | import tempfile
13 | from pathlib import Path
14 | import sys
15 |
16 | argv = sys.argv
17 | argv = argv[argv.index("--") + 1 :]
18 | bvh_file_name = argv[0]
19 |
20 | bpy.data.objects.remove(bpy.data.objects["Cube"], do_unlink=True)
21 |
22 | bpy.ops.import_scene.fbx(
23 | filepath="/queue/gesturer_mesh.fbx",
24 | global_scale=1,
25 | automatic_bone_orientation=True,
26 | axis_up="Y",
27 | )
28 |
29 | scene = context.scene
30 |
31 |
32 | fbx_model = scene.objects["Armature"]
33 |
34 |
35 | camera = bpy.data.objects["Camera"]
36 | camera.location = (0, -1.54, 0.42)
37 | camera.rotation_euler = (1.57, 0.0, 0)
38 |
39 | lamp = bpy.data.objects["Light"]
40 | lamp.location = (0.0, -6, 0)
41 |
42 |
43 | if not fbx_model.animation_data:
44 | fbx_model.animation_data_create()
45 | fbx_model.animation_data.action = None
46 |
47 | mat = bpy.data.materials["Material"]
48 |
49 |
50 | def fix_obj(parent_obj):
51 | for obj in parent_obj.children:
52 | fix_obj(obj)
53 | parent_obj.rotation_euler.x = 0
54 | if parent_obj.name in ["pCube0", "pCube1", "pCube2"]:
55 | parent_obj.location.y = -13
56 | if parent_obj.name == "pCube3":
57 | parent_obj.location.y = -10
58 | if parent_obj.name == "pCube5":
59 | parent_obj.location.y = -9.5
60 |
61 | if "materials" in dir(parent_obj.data):
62 | if parent_obj.data.materials:
63 | parent_obj.data.materials[0] = mat
64 | else:
65 | parent_obj.data.materials.append(mat)
66 |
67 |
68 | fix_obj(fbx_model)
69 |
70 | old_objs = set(scene.objects)
71 | res = bpy.ops.import_anim.bvh(filepath=bvh_file_name, global_scale=0.01,)
72 | # use_fps_scale=True, update_scene_fps=True, update_scene_duration=True,
73 |
74 | (bvh_obj,) = set(context.scene.objects) - old_objs
75 |
76 | for pb in fbx_model.pose.bones:
77 | ct = pb.constraints.new("COPY_ROTATION")
78 | ct.owner_space = "WORLD"
79 | ct.target_space = "WORLD"
80 | ct.name = pb.name
81 | ct.target = bvh_obj
82 | ct.subtarget = pb.name
83 |
84 |
85 | action = bvh_obj.animation_data.action
86 | total_frames = action.frame_range.y
87 | f = action.frame_range.x
88 |
89 | # add a keyframe to each frame of new rig
90 | while f < total_frames:
91 | scene.frame_set(f)
92 | for pb in fbx_model.pose.bones:
93 | m = fbx_model.convert_space(pose_bone=pb, matrix=pb.matrix, to_space="LOCAL")
94 |
95 | if pb.rotation_mode == "QUATERNION":
96 | pb.rotation_quaternion = m.to_quaternion()
97 | pb.keyframe_insert("rotation_quaternion", frame=f)
98 | else:
99 | pb.rotation_euler = m.to_euler(pb.rotation_mode)
100 | pb.keyframe_insert("rotation_euler", frame=f)
101 | # pb.location = m.to_translation()
102 |
103 | pb.keyframe_insert("location", frame=f)
104 | f += 1
105 |
106 | print(f"total_frames {total_frames}", flush=True)
107 |
108 |
109 | bpy.context.scene.frame_end = total_frames
110 |
111 | tmp_dir = Path(tempfile.mkdtemp()) / "video"
112 |
113 | render = bpy.context.scene.render
114 |
115 | render.filepath = str(tmp_dir)
116 |
117 | # Set render engine (this one seems to be the fastest)
118 | render.engine = "BLENDER_WORKBENCH"
119 |
120 | # Set output format
121 | render.image_settings.file_format = "FFMPEG"
122 | render.ffmpeg.format = "MPEG4"
123 |
124 | # Set the codec
125 | render.ffmpeg.codec = "H264"
126 |
127 | # Set the output resolution
128 | render.resolution_x = int(os.environ["RENDER_RESOLUTION_X"])
129 | render.resolution_y = int(os.environ["RENDER_RESOLUTION_Y"])
130 |
131 | render.fps = int(os.environ["RENDER_FPS"])
132 |
133 | bpy.ops.render.render(animation=True, write_still=False)
134 |
135 | print("output_file", str(list(tmp_dir.parent.glob("*"))[0]), flush=True)
136 |
--------------------------------------------------------------------------------
/celery-queue/gesturer_mesh.fbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jonepatr/genea_visualizer/e9284427ec32bc3c97c5db0f91e895c2a19b24ab/celery-queue/gesturer_mesh.fbx
--------------------------------------------------------------------------------
/celery-queue/requirements.txt:
--------------------------------------------------------------------------------
1 | amqp==2.2.2
2 | asn1crypto==0.24.0
3 | astunparse==1.6.3
4 | attrs==19.3.0
5 | Babel==2.9.1
6 | billiard==3.5.0.3
7 | bvh==0.3
8 | celery==4.2.1
9 | certifi==2020.6.20
10 | chardet==3.0.4
11 | cryptography==3.3.2
12 | EasyProcess==0.3
13 | flower==0.9.2
14 | idna==2.6
15 | keyring==10.6.0
16 | keyrings.alt==3.0
17 | kombu==4.2.0
18 | MarkupSafe==1.1.1
19 | Parsley==1.3
20 | piglet==1.0.0
21 | piglet-templates==1.0.0
22 | pycrypto==2.6.1
23 | pygobject==3.26.1
24 | pytz==2018.3
25 | PyVirtualDisplay==1.3.2
26 | pyxdg==0.26
27 | redis==2.10.6
28 | requests==2.24.0
29 | SecretStorage==2.3.1
30 | six==1.11.0
31 | tornado==5.0.2
32 | urllib3==1.26.5
33 | vine==1.1.4
34 |
--------------------------------------------------------------------------------
/celery-queue/tasks.py:
--------------------------------------------------------------------------------
1 | # Copyright 2020 by Patrik Jonell.
2 | # All rights reserved.
3 | # This file is part of the GENEA visualizer,
4 | # and is released under the GPLv3 License. Please see the LICENSE
5 | # file that should have been included as part of this package.
6 |
7 |
8 | import os
9 | from celery import Celery
10 | import subprocess
11 | from celery.utils.log import get_task_logger
12 | import requests
13 | import tempfile
14 | from pyvirtualdisplay import Display
15 | from bvh import Bvh
16 |
17 | Display().start()
18 |
19 |
20 | logger = get_task_logger(__name__)
21 |
22 |
23 | WORKER_TIMEOUT = int(os.environ["WORKER_TIMEOUT"])
24 | celery = Celery(
25 | "tasks",
26 | broker=os.environ["CELERY_BROKER_URL"],
27 | backend=os.environ["CELERY_RESULT_BACKEND"],
28 | )
29 |
30 |
31 | class TaskFailure(Exception):
32 | pass
33 |
34 |
35 | def validate_bvh_file(bvh_file):
36 | MAX_NUMBER_FRAMES = int(os.environ["MAX_NUMBER_FRAMES"])
37 | FRAME_TIME = 1.0 / float(os.environ["RENDER_FPS"])
38 |
39 | file_content = bvh_file.decode("utf-8")
40 | mocap = Bvh(file_content)
41 | counter = None
42 | for line in file_content.split("\n"):
43 | if counter is not None and line.strip():
44 | counter += 1
45 | if line.strip() == "MOTION":
46 | counter = -2
47 |
48 | if mocap.nframes != counter:
49 | raise TaskFailure(
50 | f"The number of rows with motion data ({counter}) does not match the Frames field ({mocap.nframes})"
51 | )
52 |
53 | if MAX_NUMBER_FRAMES != -1 and mocap.nframes > MAX_NUMBER_FRAMES:
54 | raise TaskFailure(
55 | f"The supplied number of frames ({mocap.nframes}) is bigger than {MAX_NUMBER_FRAMES}"
56 | )
57 |
58 | if mocap.frame_time != FRAME_TIME:
59 | raise TaskFailure(
60 | f"The supplied frame time ({mocap.frame_time}) differs from the required {FRAME_TIME}"
61 | )
62 |
63 |
64 | @celery.task(name="tasks.render", bind=True, hard_time_limit=WORKER_TIMEOUT)
65 | def render(self, bvh_file_uri: str) -> str:
66 | HEADERS = {"Authorization": f"Bearer " + os.environ["SYSTEM_TOKEN"]}
67 | API_SERVER = os.environ["API_SERVER"]
68 |
69 | logger.info("rendering..")
70 | self.update_state(state="PROCESSING")
71 |
72 | bvh_file = requests.get(API_SERVER + bvh_file_uri, headers=HEADERS).content
73 | validate_bvh_file(bvh_file)
74 |
75 | with tempfile.NamedTemporaryFile(suffix=".bhv") as tmpf:
76 | tmpf.write(bvh_file)
77 | tmpf.seek(0)
78 |
79 | process = subprocess.Popen(
80 | [
81 | "/blender/blender-2.83.0-linux64/blender",
82 | "-noaudio",
83 | "-b",
84 | "--python",
85 | "blender_render.py",
86 | "--",
87 | tmpf.name,
88 | ],
89 | stdout=subprocess.PIPE,
90 | stderr=subprocess.PIPE,
91 | )
92 | total = None
93 | current_frame = None
94 | for line in process.stdout:
95 | line = line.decode("utf-8").strip()
96 | if line.startswith("total_frames "):
97 | _, total = line.split(" ")
98 | total = int(float(total))
99 | elif line.startswith("Append frame "):
100 | *_, current_frame = line.split(" ")
101 | current_frame = int(current_frame)
102 | elif line.startswith("output_file"):
103 | _, file_name = line.split(" ")
104 | files = {"file": (os.path.basename(file_name), open(file_name, "rb"))}
105 | return requests.post(
106 | API_SERVER + "/upload_video", files=files, headers=HEADERS
107 | ).text
108 | if total and current_frame:
109 | self.update_state(
110 | state="RENDERING", meta={"current": current_frame, "total": total}
111 | )
112 | if process.returncode != 0:
113 | raise TaskFailure(process.stderr.read().decode("utf-8"))
114 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | web:
4 | environment:
5 | - SYSTEM_TOKEN=${SYSTEM_TOKEN}
6 | - USER_TOKEN=${USER_TOKEN}
7 | - CELERY_BROKER_URL=${CELERY_BROKER_URL}
8 | - CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
9 | - INTERNAL_API_PORT=${INTERNAL_API_PORT}
10 | ports:
11 | - ${PUBLIC_WEB_PORT}:${INTERNAL_API_PORT}
12 | build:
13 | context: ./api
14 | dockerfile: Dockerfile
15 | restart: always
16 | depends_on:
17 | - redis
18 | worker:
19 | environment:
20 | - SYSTEM_TOKEN=${SYSTEM_TOKEN}
21 | - API_SERVER=http://web:${INTERNAL_API_PORT}
22 | - CELERY_BROKER_URL=${CELERY_BROKER_URL}
23 | - CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
24 | - RENDER_RESOLUTION_X=${RENDER_RESOLUTION_X}
25 | - RENDER_RESOLUTION_Y=${RENDER_RESOLUTION_Y}
26 | - RENDER_FPS=${RENDER_FPS}
27 | - MAX_NUMBER_FRAMES=${MAX_NUMBER_FRAMES}
28 | - WORKER_TIMEOUT=${WORKER_TIMEOUT}
29 | build:
30 | context: celery-queue
31 | dockerfile: Dockerfile
32 | depends_on:
33 | - redis
34 | monitor:
35 | environment:
36 | - CELERY_BROKER_URL=${CELERY_BROKER_URL}
37 | - CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
38 | - WORKER_TIMEOUT=${WORKER_TIMEOUT}
39 | build:
40 | context: celery-queue
41 | dockerfile: Dockerfile
42 | ports:
43 | - ${PUBLIC_MONITOR_PORT}:${INTERNAL_MONITOR_PORT}
44 | entrypoint: flower
45 | command: -A tasks --port=${INTERNAL_MONITOR_PORT} --broker=${CELERY_BROKER_URL} --basic_auth=${FLOWER_USER}:${FLOWER_PWD}
46 | depends_on:
47 | - redis
48 | redis:
49 | image: redis
--------------------------------------------------------------------------------
/example.py:
--------------------------------------------------------------------------------
1 | # Copyright 2020 by Patrik Jonell.
2 | # All rights reserved.
3 | # This file is part of the GENEA visualizer,
4 | # and is released under the GPLv3 License. Please see the LICENSE
5 | # file that should have been included as part of this package.
6 |
7 |
8 | import requests
9 | from pathlib import Path
10 | import time
11 |
12 | import argparse
13 |
14 | parser = argparse.ArgumentParser()
15 | parser.add_argument("bvh_file", type=Path)
16 | parser.add_argument("--server_url", default="http://localhost:5001")
17 | parser.add_argument("--output", type=Path)
18 |
19 | args = parser.parse_args()
20 |
21 | server_url = args.server_url
22 | bvh_file = args.bvh_file
23 | output = args.output if args.output else bvh_file.with_suffix(".mp4")
24 |
25 | headers = {"Authorization": "Bearer j7HgTkwt24yKWfHPpFG3eoydJK6syAsz"}
26 |
27 |
28 | render_request = requests.post(
29 | f"{server_url}/render",
30 | files={"file": (bvh_file.name, bvh_file.open())},
31 | headers=headers,
32 | )
33 | job_uri = render_request.text
34 |
35 | done = False
36 | while not done:
37 | resp = requests.get(server_url + job_uri, headers=headers)
38 | resp.raise_for_status()
39 |
40 | response = resp.json()
41 |
42 | if response["state"] == "PENDING":
43 | jobs_in_queue = response["result"]["jobs_in_queue"]
44 | print(f"pending.. {jobs_in_queue} jobs currently in queue")
45 |
46 | elif response["state"] == "PROCESSING":
47 | print("Processing the file (this can take a while depending on file size)")
48 |
49 | elif response["state"] == "RENDERING":
50 | current = response["result"]["current"]
51 | total = response["result"]["total"]
52 | print(f"currently rendering, {current}/{total} done")
53 |
54 | elif response["state"] == "SUCCESS":
55 | file_url = response["result"]
56 | done = True
57 | break
58 |
59 | elif response["state"] == "FAILURE":
60 | raise Exception(response["result"])
61 | else:
62 | print(response)
63 | raise Exception("should not happen..")
64 | time.sleep(10)
65 |
66 |
67 | video = requests.get(server_url + file_url, headers=headers).content
68 | output.write_bytes(video)
69 |
--------------------------------------------------------------------------------
/gesture.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jonepatr/genea_visualizer/e9284427ec32bc3c97c5db0f91e895c2a19b24ab/gesture.gif
--------------------------------------------------------------------------------