motion-studio 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- motion_studio/__init__.py +52 -0
- motion_studio/__main__.py +8 -0
- motion_studio/bundle.py +424 -0
- motion_studio/cli.py +459 -0
- motion_studio/config.py +58 -0
- motion_studio/core/__init__.py +8 -0
- motion_studio/core/plugins.py +285 -0
- motion_studio/core/types.py +72 -0
- motion_studio/library.py +188 -0
- motion_studio/plugins_builtin/__init__.py +8 -0
- motion_studio/plugins_builtin/_convert.py +108 -0
- motion_studio/plugins_builtin/corrector.py +79 -0
- motion_studio/plugins_builtin/corrector_impl.py +992 -0
- motion_studio/plugins_builtin/metrics.py +62 -0
- motion_studio/plugins_builtin/metrics_live.py +135 -0
- motion_studio/plugins_builtin/utils/__init__.py +74 -0
- motion_studio/plugins_builtin/utils/floor_utils.py +269 -0
- motion_studio/plugins_builtin/utils/metrics_utils.py +599 -0
- motion_studio/plugins_builtin/utils/motion_utils.py +880 -0
- motion_studio/server/__init__.py +1 -0
- motion_studio/server/api_bundle.py +649 -0
- motion_studio/server/api_motion.py +622 -0
- motion_studio/server/api_video.py +182 -0
- motion_studio/server/app.py +185 -0
- motion_studio/server/common.py +119 -0
- motion_studio/server/loaders.py +197 -0
- motion_studio/server/state.py +254 -0
- motion_studio/server/video_cache.py +283 -0
- motion_studio/smpl/__init__.py +5 -0
- motion_studio/smpl/convert.py +363 -0
- motion_studio/smpl/io.py +86 -0
- motion_studio/smpl/refit.py +370 -0
- motion_studio/static/app.js +4513 -0
- motion_studio/static/index.html +386 -0
- motion_studio/static/style.css +340 -0
- motion_studio/static/vendor/OrbitControls.js +1417 -0
- motion_studio/static/vendor/TransformControls.js +1573 -0
- motion_studio/static/vendor/three.module.js +53044 -0
- motion_studio-0.1.0.dist-info/METADATA +295 -0
- motion_studio-0.1.0.dist-info/RECORD +44 -0
- motion_studio-0.1.0.dist-info/WHEEL +5 -0
- motion_studio-0.1.0.dist-info/entry_points.txt +2 -0
- motion_studio-0.1.0.dist-info/licenses/LICENSE +21 -0
- motion_studio-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Motion Studio: a clean, installable multi-person SMPL motion editor.
|
|
2
|
+
|
|
3
|
+
The names re-exported here are the package's stable public API. Everything in
|
|
4
|
+
this top-level namespace is torch-free and safe to import in a numpy+flask-only
|
|
5
|
+
("core") install: the heavy server stack and the built-in torch/SMPL plugins
|
|
6
|
+
are imported lazily, never at package import time.
|
|
7
|
+
|
|
8
|
+
Typical use::
|
|
9
|
+
|
|
10
|
+
import motion_studio as ms
|
|
11
|
+
|
|
12
|
+
bundle = ms.load_bundle("session.motion")
|
|
13
|
+
motion = bundle.original # ms.Motion
|
|
14
|
+
corrector = ms.load_corrector(ms.Config().corrector_spec,
|
|
15
|
+
smpl_dir="~/smpl/models")
|
|
16
|
+
fixed = corrector.correct(motion)
|
|
17
|
+
ms.save_motion_pkl(fixed, "fixed.pkl")
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .bundle import load_bundle, save_bundle
|
|
22
|
+
from .config import Config
|
|
23
|
+
from .core.plugins import (MotionCorrector, MotionMetrics, load_corrector,
|
|
24
|
+
load_metrics)
|
|
25
|
+
from .core.types import Floor, Motion
|
|
26
|
+
from .library import import_dataset, scan_dataset
|
|
27
|
+
from .smpl.io import load_motion_pkl, save_motion_pkl
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# Core data types.
|
|
33
|
+
"Motion",
|
|
34
|
+
"Floor",
|
|
35
|
+
# Plugin contracts + loaders.
|
|
36
|
+
"MotionCorrector",
|
|
37
|
+
"MotionMetrics",
|
|
38
|
+
"load_corrector",
|
|
39
|
+
"load_metrics",
|
|
40
|
+
# SMPL pkl I/O.
|
|
41
|
+
"load_motion_pkl",
|
|
42
|
+
"save_motion_pkl",
|
|
43
|
+
# .motion bundle I/O.
|
|
44
|
+
"save_bundle",
|
|
45
|
+
"load_bundle",
|
|
46
|
+
# Dataset discovery / import.
|
|
47
|
+
"scan_dataset",
|
|
48
|
+
"import_dataset",
|
|
49
|
+
# Configuration.
|
|
50
|
+
"Config",
|
|
51
|
+
"__version__",
|
|
52
|
+
]
|
motion_studio/bundle.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""The ``.motion`` save-file format for a Motion Studio editing session.
|
|
2
|
+
|
|
3
|
+
A ``.motion`` file is a single ZIP archive that bundles everything about one
|
|
4
|
+
editing session: the original SMPL motion, the edited motion (if any), the
|
|
5
|
+
source video and music, plus all the side metadata the editor needs to reopen
|
|
6
|
+
the session exactly where the user left off (camera/video placement, comments,
|
|
7
|
+
metrics, ...). Reopening a bundle restores the full session; a caller that only
|
|
8
|
+
wants the SMPL can read ``original``/``edited`` and ignore the rest.
|
|
9
|
+
|
|
10
|
+
The archive layout is::
|
|
11
|
+
|
|
12
|
+
manifest.json session metadata (see save_bundle for the schema)
|
|
13
|
+
motion_original.npz poses, trans, betas (+ scalar fields) of the original
|
|
14
|
+
motion_edited.npz same, for the edited motion (absent if no edits)
|
|
15
|
+
video.mp4 source video bytes (optional)
|
|
16
|
+
music.<ext> music bytes, original extension (optional)
|
|
17
|
+
thumbnail.png preview image (optional)
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import dataclasses
|
|
22
|
+
import datetime
|
|
23
|
+
import io
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import zipfile
|
|
27
|
+
from typing import Any, Dict, List, Optional, Union
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
from motion_studio.core.types import Motion
|
|
32
|
+
|
|
33
|
+
MOTION_EXT = ".motion"
|
|
34
|
+
|
|
35
|
+
_FORMAT = "motion-studio"
|
|
36
|
+
_VERSION = 1
|
|
37
|
+
_MANIFEST_NAME = "manifest.json"
|
|
38
|
+
_ORIGINAL_NAME = "motion_original.npz"
|
|
39
|
+
_EDITED_NAME = "motion_edited.npz"
|
|
40
|
+
_VIDEO_NAME = "video.mp4"
|
|
41
|
+
_THUMBNAIL_NAME = "thumbnail.png"
|
|
42
|
+
_MUSIC_STEM = "music"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclasses.dataclass
|
|
46
|
+
class Bundle:
|
|
47
|
+
"""A loaded ``.motion`` editing session.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
original: The original (unedited) motion.
|
|
51
|
+
edited: The edited motion, or None if the session had no edits.
|
|
52
|
+
video: Raw source video bytes, or None.
|
|
53
|
+
music: Raw music bytes, or None.
|
|
54
|
+
music_ext: Extension of the music file, without the leading dot.
|
|
55
|
+
video_params: Video placement params (posX, posY, scale, ...).
|
|
56
|
+
comments: List of user comments attached to the session.
|
|
57
|
+
metrics: Mapping {"ref": {...}, "cur": {...}} of metric values.
|
|
58
|
+
manifest: The full manifest dict, as stored in the archive.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
original: Motion
|
|
62
|
+
edited: Optional[Motion]
|
|
63
|
+
video: Optional[bytes]
|
|
64
|
+
music: Optional[bytes]
|
|
65
|
+
music_ext: str
|
|
66
|
+
video_params: Dict[str, Any]
|
|
67
|
+
comments: List[Any]
|
|
68
|
+
metrics: Dict[str, Any]
|
|
69
|
+
manifest: Dict[str, Any]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclasses.dataclass
|
|
73
|
+
class BundleMeta:
|
|
74
|
+
"""A cheaply loaded ``.motion`` session: motions + metadata, lazy media.
|
|
75
|
+
|
|
76
|
+
Reading a bundle's ``video.mp4`` / music bytes can be tens of megabytes;
|
|
77
|
+
the hot playback path (``/mesh_frame``, ``/mesh_faces``, ``resolve_motion``)
|
|
78
|
+
only needs the SMPL motions and the manifest. :func:`load_bundle_meta`
|
|
79
|
+
decodes just the manifest and the small ``.npz`` motion members and leaves
|
|
80
|
+
the heavy video/music bytes behind lazy accessors, so a playback request no
|
|
81
|
+
longer re-reads the whole archive.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
original: The original (unedited) motion.
|
|
85
|
+
edited: The edited motion, or None if the session had no edits.
|
|
86
|
+
music_ext: Extension of the music file, without the leading dot.
|
|
87
|
+
video_params: Video placement params (posX, posY, scale, ...).
|
|
88
|
+
comments: List of user comments attached to the session.
|
|
89
|
+
metrics: Mapping {"ref": {...}, "cur": {...}} of metric values.
|
|
90
|
+
manifest: The full manifest dict, as stored in the archive.
|
|
91
|
+
has_video: Whether the archive carries a ``video.mp4`` member.
|
|
92
|
+
has_music: Whether the archive carries a music member.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
original: Motion
|
|
96
|
+
edited: Optional[Motion]
|
|
97
|
+
music_ext: str
|
|
98
|
+
video_params: Dict[str, Any]
|
|
99
|
+
comments: List[Any]
|
|
100
|
+
metrics: Dict[str, Any]
|
|
101
|
+
manifest: Dict[str, Any]
|
|
102
|
+
has_video: bool
|
|
103
|
+
has_music: bool
|
|
104
|
+
_path: str
|
|
105
|
+
|
|
106
|
+
def video_bytes(self) -> Optional[bytes]:
|
|
107
|
+
"""Read and return the archive's ``video.mp4`` bytes, or None."""
|
|
108
|
+
if not self.has_video:
|
|
109
|
+
return None
|
|
110
|
+
with zipfile.ZipFile(self._path, "r") as zf:
|
|
111
|
+
if _VIDEO_NAME not in set(zf.namelist()):
|
|
112
|
+
return None
|
|
113
|
+
return zf.read(_VIDEO_NAME)
|
|
114
|
+
|
|
115
|
+
def music_bytes(self) -> Optional[bytes]:
|
|
116
|
+
"""Read and return the archive's music bytes, or None."""
|
|
117
|
+
if not self.has_music:
|
|
118
|
+
return None
|
|
119
|
+
music_name = "%s.%s" % (_MUSIC_STEM, self.music_ext)
|
|
120
|
+
with zipfile.ZipFile(self._path, "r") as zf:
|
|
121
|
+
if music_name not in set(zf.namelist()):
|
|
122
|
+
return None
|
|
123
|
+
return zf.read(music_name)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def motion_to_npz_dict(m: Motion) -> Dict[str, np.ndarray]:
|
|
127
|
+
"""Serialize a Motion to a flat dict of arrays for ``np.savez``.
|
|
128
|
+
|
|
129
|
+
Scalar fields (gender, fps, name) are stored as 0-d arrays so they survive
|
|
130
|
+
the round-trip through an ``.npz`` archive. ``betas`` is stored only when
|
|
131
|
+
present; its absence is recorded by a ``has_betas`` flag.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
m: The motion to serialize.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
A mapping of archive member name to numpy array.
|
|
138
|
+
"""
|
|
139
|
+
out: Dict[str, np.ndarray] = {
|
|
140
|
+
"poses": np.asarray(m.poses),
|
|
141
|
+
"trans": np.asarray(m.trans),
|
|
142
|
+
"gender": np.asarray(m.gender),
|
|
143
|
+
"fps": np.asarray(float(m.fps)),
|
|
144
|
+
"name": np.asarray(m.name),
|
|
145
|
+
"has_betas": np.asarray(m.betas is not None),
|
|
146
|
+
}
|
|
147
|
+
if m.betas is not None:
|
|
148
|
+
out["betas"] = np.asarray(m.betas)
|
|
149
|
+
return out
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def motion_from_npz(npz: "np.lib.npyio.NpzFile") -> Motion:
|
|
153
|
+
"""Rebuild a Motion from an opened ``.npz`` produced by this module.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
npz: A mapping (e.g. an open ``NpzFile``) with the keys written by
|
|
157
|
+
``motion_to_npz_dict``.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The reconstructed motion.
|
|
161
|
+
"""
|
|
162
|
+
betas = npz["betas"] if bool(npz["has_betas"]) else None
|
|
163
|
+
return Motion(
|
|
164
|
+
poses=np.asarray(npz["poses"]),
|
|
165
|
+
trans=np.asarray(npz["trans"]),
|
|
166
|
+
betas=None if betas is None else np.asarray(betas),
|
|
167
|
+
gender=str(npz["gender"]),
|
|
168
|
+
fps=float(npz["fps"]),
|
|
169
|
+
name=str(npz["name"]),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _read_bytes(data: Union[bytes, str]) -> bytes:
|
|
174
|
+
"""Return ``data`` as bytes, reading from disk if it is a path.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
data: Raw bytes, or a filesystem path to read.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The raw bytes.
|
|
181
|
+
"""
|
|
182
|
+
if isinstance(data, (bytes, bytearray)):
|
|
183
|
+
return bytes(data)
|
|
184
|
+
with open(data, "rb") as f:
|
|
185
|
+
return f.read()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _motion_bytes(m: Motion) -> bytes:
|
|
189
|
+
"""Serialize a Motion to the raw bytes of an ``.npz`` archive."""
|
|
190
|
+
buffer = io.BytesIO()
|
|
191
|
+
np.savez(buffer, **motion_to_npz_dict(m))
|
|
192
|
+
return buffer.getvalue()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _load_motion_member(zf: zipfile.ZipFile, member: str) -> Motion:
|
|
196
|
+
"""Read and deserialize a Motion from a member of an open archive."""
|
|
197
|
+
with io.BytesIO(zf.read(member)) as buffer:
|
|
198
|
+
with np.load(buffer, allow_pickle=False) as npz:
|
|
199
|
+
return motion_from_npz(npz)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def save_bundle(
|
|
203
|
+
path: str,
|
|
204
|
+
*,
|
|
205
|
+
original: Motion,
|
|
206
|
+
edited: Optional[Motion] = None,
|
|
207
|
+
video: Optional[Union[bytes, str]] = None,
|
|
208
|
+
music: Optional[Union[bytes, str]] = None,
|
|
209
|
+
music_ext: str = "wav",
|
|
210
|
+
video_params: Optional[Dict[str, Any]] = None,
|
|
211
|
+
comments: Optional[List[Any]] = None,
|
|
212
|
+
metrics: Optional[Dict[str, Any]] = None,
|
|
213
|
+
source_clip: str = "",
|
|
214
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
215
|
+
thumbnail: Optional[Union[bytes, str]] = None,
|
|
216
|
+
created: Optional[str] = None,
|
|
217
|
+
modified: Optional[str] = None,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Write a full editing session to a ``.motion`` (ZIP) file.
|
|
220
|
+
|
|
221
|
+
The manifest stored in the archive has the schema::
|
|
222
|
+
|
|
223
|
+
{
|
|
224
|
+
"format": "motion-studio",
|
|
225
|
+
"version": 1,
|
|
226
|
+
"name": str,
|
|
227
|
+
"source_clip": str,
|
|
228
|
+
"created": ISO-8601 str,
|
|
229
|
+
"modified": ISO-8601 str,
|
|
230
|
+
"comments": [...],
|
|
231
|
+
"video_params": {"posX": ..., "scale": ..., ...},
|
|
232
|
+
"bg_removed": bool,
|
|
233
|
+
"metrics": {"ref": {...}, "cur": {...}},
|
|
234
|
+
"has_video": bool,
|
|
235
|
+
"has_music": bool,
|
|
236
|
+
"music_ext": str,
|
|
237
|
+
"gender": str,
|
|
238
|
+
"fps": float,
|
|
239
|
+
"n_persons": int,
|
|
240
|
+
"n_frames": int,
|
|
241
|
+
"extra": {...}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
path: Destination path for the ``.motion`` file.
|
|
246
|
+
original: The original (unedited) motion; required.
|
|
247
|
+
edited: The edited motion, or None to omit it from the archive.
|
|
248
|
+
video: Source video as raw bytes or a filesystem path, or None.
|
|
249
|
+
music: Music as raw bytes or a filesystem path, or None.
|
|
250
|
+
music_ext: Extension to store the music under, without the dot.
|
|
251
|
+
video_params: Video placement params (posX, posY, scale, ...).
|
|
252
|
+
comments: User comments to attach to the session.
|
|
253
|
+
metrics: Mapping {"ref": {...}, "cur": {...}} of metric values.
|
|
254
|
+
source_clip: Identifier of the clip the session originated from.
|
|
255
|
+
extra: Arbitrary extra JSON-serializable metadata.
|
|
256
|
+
thumbnail: Preview image as raw bytes or a path, or None.
|
|
257
|
+
created: Creation timestamp (ISO-8601); defaults to now (UTC).
|
|
258
|
+
modified: Last-modified timestamp (ISO-8601); defaults to now (UTC).
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
None.
|
|
262
|
+
"""
|
|
263
|
+
video_params = dict(video_params or {})
|
|
264
|
+
comments = list(comments or [])
|
|
265
|
+
metrics = dict(metrics or {})
|
|
266
|
+
extra = dict(extra or {})
|
|
267
|
+
music_ext = music_ext.lstrip(".") or "wav"
|
|
268
|
+
|
|
269
|
+
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
270
|
+
created = created or now
|
|
271
|
+
modified = modified or now
|
|
272
|
+
|
|
273
|
+
video_bytes = None if video is None else _read_bytes(video)
|
|
274
|
+
music_bytes = None if music is None else _read_bytes(music)
|
|
275
|
+
thumb_bytes = None if thumbnail is None else _read_bytes(thumbnail)
|
|
276
|
+
|
|
277
|
+
manifest: Dict[str, Any] = {
|
|
278
|
+
"format": _FORMAT,
|
|
279
|
+
"version": _VERSION,
|
|
280
|
+
"name": original.name,
|
|
281
|
+
"source_clip": source_clip,
|
|
282
|
+
"created": created,
|
|
283
|
+
"modified": modified,
|
|
284
|
+
"comments": comments,
|
|
285
|
+
"video_params": video_params,
|
|
286
|
+
"bg_removed": bool(video_params.get("bg_removed", False)),
|
|
287
|
+
"metrics": metrics,
|
|
288
|
+
"has_video": video_bytes is not None,
|
|
289
|
+
"has_music": music_bytes is not None,
|
|
290
|
+
"music_ext": music_ext,
|
|
291
|
+
"gender": original.gender,
|
|
292
|
+
"fps": float(original.fps),
|
|
293
|
+
"n_persons": original.n_persons,
|
|
294
|
+
"n_frames": original.n_frames,
|
|
295
|
+
"extra": extra,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
299
|
+
zf.writestr(
|
|
300
|
+
_MANIFEST_NAME,
|
|
301
|
+
json.dumps(manifest, indent=2, sort_keys=True),
|
|
302
|
+
)
|
|
303
|
+
zf.writestr(_ORIGINAL_NAME, _motion_bytes(original))
|
|
304
|
+
if edited is not None:
|
|
305
|
+
zf.writestr(_EDITED_NAME, _motion_bytes(edited))
|
|
306
|
+
if video_bytes is not None:
|
|
307
|
+
zf.writestr(_VIDEO_NAME, video_bytes)
|
|
308
|
+
if music_bytes is not None:
|
|
309
|
+
zf.writestr("%s.%s" % (_MUSIC_STEM, music_ext), music_bytes)
|
|
310
|
+
if thumb_bytes is not None:
|
|
311
|
+
zf.writestr(_THUMBNAIL_NAME, thumb_bytes)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _read_manifest(zf: zipfile.ZipFile) -> Dict[str, Any]:
|
|
315
|
+
"""Read and validate the manifest of an open ``.motion`` archive.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
zf: An open ``.motion`` ZIP archive.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
The parsed manifest dict.
|
|
322
|
+
|
|
323
|
+
Raises:
|
|
324
|
+
ValueError: If the archive has no manifest member, or its ``format`` is
|
|
325
|
+
not a Motion Studio bundle.
|
|
326
|
+
"""
|
|
327
|
+
if _MANIFEST_NAME not in set(zf.namelist()):
|
|
328
|
+
raise ValueError("not a Motion Studio bundle: missing manifest.json")
|
|
329
|
+
manifest = json.loads(zf.read(_MANIFEST_NAME).decode("utf-8"))
|
|
330
|
+
if manifest.get("format") != _FORMAT:
|
|
331
|
+
raise ValueError(
|
|
332
|
+
"not a Motion Studio bundle: %r" % (manifest.get("format"),))
|
|
333
|
+
return manifest
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def load_bundle_meta(path: str) -> BundleMeta:
|
|
337
|
+
"""Read a ``.motion`` file's motions + metadata, leaving media lazy.
|
|
338
|
+
|
|
339
|
+
The cheap counterpart to :func:`load_bundle`: it decodes only the manifest
|
|
340
|
+
and the small ``.npz`` motion members, never the (potentially large) video
|
|
341
|
+
or music bytes. Use it on the hot path (``resolve_motion`` / playback) and
|
|
342
|
+
pull media on demand via :meth:`BundleMeta.video_bytes` /
|
|
343
|
+
:meth:`BundleMeta.music_bytes`.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
path: Path to the ``.motion`` file to read.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
The reconstructed :class:`BundleMeta`.
|
|
350
|
+
|
|
351
|
+
Raises:
|
|
352
|
+
ValueError: If the archive is not a Motion Studio bundle (bad/absent
|
|
353
|
+
manifest).
|
|
354
|
+
KeyError: If the mandatory original motion member is missing.
|
|
355
|
+
"""
|
|
356
|
+
with zipfile.ZipFile(path, "r") as zf:
|
|
357
|
+
names = set(zf.namelist())
|
|
358
|
+
manifest = _read_manifest(zf)
|
|
359
|
+
original = _load_motion_member(zf, _ORIGINAL_NAME)
|
|
360
|
+
edited = (
|
|
361
|
+
_load_motion_member(zf, _EDITED_NAME)
|
|
362
|
+
if _EDITED_NAME in names else None
|
|
363
|
+
)
|
|
364
|
+
music_ext = manifest.get("music_ext", "wav")
|
|
365
|
+
music_name = "%s.%s" % (_MUSIC_STEM, music_ext)
|
|
366
|
+
has_video = _VIDEO_NAME in names
|
|
367
|
+
has_music = music_name in names
|
|
368
|
+
|
|
369
|
+
return BundleMeta(
|
|
370
|
+
original=original,
|
|
371
|
+
edited=edited,
|
|
372
|
+
music_ext=music_ext,
|
|
373
|
+
video_params=dict(manifest.get("video_params", {})),
|
|
374
|
+
comments=list(manifest.get("comments", [])),
|
|
375
|
+
metrics=dict(manifest.get("metrics", {})),
|
|
376
|
+
manifest=manifest,
|
|
377
|
+
has_video=has_video,
|
|
378
|
+
has_music=has_music,
|
|
379
|
+
_path=path,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def load_bundle(path: str) -> Bundle:
|
|
384
|
+
"""Read a ``.motion`` file back into a full Bundle (media included).
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
path: Path to the ``.motion`` file to read.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
The reconstructed Bundle.
|
|
391
|
+
|
|
392
|
+
Raises:
|
|
393
|
+
ValueError: If the archive is not a Motion Studio bundle (bad/absent
|
|
394
|
+
manifest).
|
|
395
|
+
KeyError: If the mandatory original motion member is missing.
|
|
396
|
+
"""
|
|
397
|
+
with zipfile.ZipFile(path, "r") as zf:
|
|
398
|
+
names = set(zf.namelist())
|
|
399
|
+
|
|
400
|
+
manifest = _read_manifest(zf)
|
|
401
|
+
|
|
402
|
+
original = _load_motion_member(zf, _ORIGINAL_NAME)
|
|
403
|
+
edited = (
|
|
404
|
+
_load_motion_member(zf, _EDITED_NAME)
|
|
405
|
+
if _EDITED_NAME in names else None
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
video = zf.read(_VIDEO_NAME) if _VIDEO_NAME in names else None
|
|
409
|
+
|
|
410
|
+
music_ext = manifest.get("music_ext", "wav")
|
|
411
|
+
music_name = "%s.%s" % (_MUSIC_STEM, music_ext)
|
|
412
|
+
music = zf.read(music_name) if music_name in names else None
|
|
413
|
+
|
|
414
|
+
return Bundle(
|
|
415
|
+
original=original,
|
|
416
|
+
edited=edited,
|
|
417
|
+
video=video,
|
|
418
|
+
music=music,
|
|
419
|
+
music_ext=music_ext,
|
|
420
|
+
video_params=dict(manifest.get("video_params", {})),
|
|
421
|
+
comments=list(manifest.get("comments", [])),
|
|
422
|
+
metrics=dict(manifest.get("metrics", {})),
|
|
423
|
+
manifest=manifest,
|
|
424
|
+
)
|