media-optimization-engine 1.4.0__tar.gz
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.
- media_optimization_engine-1.4.0/LICENSE +21 -0
- media_optimization_engine-1.4.0/PKG-INFO +253 -0
- media_optimization_engine-1.4.0/README.md +197 -0
- media_optimization_engine-1.4.0/media_engine/__init__.py +7 -0
- media_optimization_engine-1.4.0/media_engine/adapters/__init__.py +0 -0
- media_optimization_engine-1.4.0/media_engine/adapters/flutter/README.md +10 -0
- media_optimization_engine-1.4.0/media_engine/adapters/flutter/__init__.py +0 -0
- media_optimization_engine-1.4.0/media_engine/adapters/flutter/models.dart +88 -0
- media_optimization_engine-1.4.0/media_engine/adapters/marzipano/__init__.py +2 -0
- media_optimization_engine-1.4.0/media_engine/adapters/marzipano/manifest.py +53 -0
- media_optimization_engine-1.4.0/media_engine/adapters/pannellum/__init__.py +2 -0
- media_optimization_engine-1.4.0/media_engine/adapters/pannellum/manifest.py +23 -0
- media_optimization_engine-1.4.0/media_engine/adapters/threejs/__init__.py +2 -0
- media_optimization_engine-1.4.0/media_engine/adapters/threejs/manifest.py +10 -0
- media_optimization_engine-1.4.0/media_engine/admin.py +36 -0
- media_optimization_engine-1.4.0/media_engine/apps.py +11 -0
- media_optimization_engine-1.4.0/media_engine/auth.py +33 -0
- media_optimization_engine-1.4.0/media_engine/autoregister.py +108 -0
- media_optimization_engine-1.4.0/media_engine/control_plane.py +274 -0
- media_optimization_engine-1.4.0/media_engine/doctor.py +177 -0
- media_optimization_engine-1.4.0/media_engine/focal_detectors.py +28 -0
- media_optimization_engine-1.4.0/media_engine/health.py +78 -0
- media_optimization_engine-1.4.0/media_engine/image_ops.py +142 -0
- media_optimization_engine-1.4.0/media_engine/integration.py +34 -0
- media_optimization_engine-1.4.0/media_engine/locks.py +20 -0
- media_optimization_engine-1.4.0/media_engine/management/__init__.py +0 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/__init__.py +0 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/audit_media_engine.py +54 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/audit_panorama_media.py +20 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/backfill_panorama_media.py +32 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/backfill_registered_media.py +21 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/backfill_responsive_images.py +27 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/cleanup_image_derivatives.py +30 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/media_engine_doctor.py +15 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/media_engine_node_report.py +24 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/rollback_media_engine_config.py +14 -0
- media_optimization_engine-1.4.0/media_engine/management/commands/sync_media_engine_hub.py +20 -0
- media_optimization_engine-1.4.0/media_engine/manifest.py +89 -0
- media_optimization_engine-1.4.0/media_engine/metrics.py +34 -0
- media_optimization_engine-1.4.0/media_engine/migrations/0001_initial.py +87 -0
- media_optimization_engine-1.4.0/media_engine/migrations/0002_panorama_multires.py +44 -0
- media_optimization_engine-1.4.0/media_engine/migrations/0003_panorama_cube_tiles.py +31 -0
- media_optimization_engine-1.4.0/media_engine/migrations/__init__.py +0 -0
- media_optimization_engine-1.4.0/media_engine/models.py +145 -0
- media_optimization_engine-1.4.0/media_engine/node.py +29 -0
- media_optimization_engine-1.4.0/media_engine/processors/__init__.py +0 -0
- media_optimization_engine-1.4.0/media_engine/processors/base.py +21 -0
- media_optimization_engine-1.4.0/media_engine/processors/image/__init__.py +0 -0
- media_optimization_engine-1.4.0/media_engine/processors/image/standard.py +16 -0
- media_optimization_engine-1.4.0/media_engine/processors/panorama/__init__.py +5 -0
- media_optimization_engine-1.4.0/media_engine/processors/panorama/cube_tiles.py +121 -0
- media_optimization_engine-1.4.0/media_engine/processors/panorama/cubemap.py +40 -0
- media_optimization_engine-1.4.0/media_engine/processors/panorama/detect.py +23 -0
- media_optimization_engine-1.4.0/media_engine/processors/panorama/equirectangular.py +149 -0
- media_optimization_engine-1.4.0/media_engine/processors/panorama/manifest.py +105 -0
- media_optimization_engine-1.4.0/media_engine/processors/panorama/tiles.py +88 -0
- media_optimization_engine-1.4.0/media_engine/processors/registry.py +17 -0
- media_optimization_engine-1.4.0/media_engine/profiles.py +77 -0
- media_optimization_engine-1.4.0/media_engine/queueing.py +34 -0
- media_optimization_engine-1.4.0/media_engine/registry.py +49 -0
- media_optimization_engine-1.4.0/media_engine/runtime_config.py +37 -0
- media_optimization_engine-1.4.0/media_engine/serializers.py +29 -0
- media_optimization_engine-1.4.0/media_engine/services.py +187 -0
- media_optimization_engine-1.4.0/media_engine/signals.py +5 -0
- media_optimization_engine-1.4.0/media_engine/spec/openapi.yaml +191 -0
- media_optimization_engine-1.4.0/media_engine/static/media_engine/adaptive-media.js +77 -0
- media_optimization_engine-1.4.0/media_engine/static/media_engine/media-cache-sw.js +17 -0
- media_optimization_engine-1.4.0/media_engine/storage_paths.py +13 -0
- media_optimization_engine-1.4.0/media_engine/system_views.py +102 -0
- media_optimization_engine-1.4.0/media_engine/tasks.py +44 -0
- media_optimization_engine-1.4.0/media_engine/templatetags/__init__.py +0 -0
- media_optimization_engine-1.4.0/media_engine/templatetags/responsive_media.py +262 -0
- media_optimization_engine-1.4.0/media_engine/tests/__init__.py +0 -0
- media_optimization_engine-1.4.0/media_engine/tests/test_distributed_node.py +72 -0
- media_optimization_engine-1.4.0/media_engine/tests/test_image_ops.py +14 -0
- media_optimization_engine-1.4.0/media_engine/tests/test_panorama.py +31 -0
- media_optimization_engine-1.4.0/media_engine/tests/test_paths.py +9 -0
- media_optimization_engine-1.4.0/media_engine/tests/test_profiles.py +8 -0
- media_optimization_engine-1.4.0/media_engine/tests/test_responsive_media.py +153 -0
- media_optimization_engine-1.4.0/media_engine/urls.py +14 -0
- media_optimization_engine-1.4.0/media_engine/validators.py +42 -0
- media_optimization_engine-1.4.0/media_engine/views.py +111 -0
- media_optimization_engine-1.4.0/media_optimization_engine.egg-info/PKG-INFO +253 -0
- media_optimization_engine-1.4.0/media_optimization_engine.egg-info/SOURCES.txt +88 -0
- media_optimization_engine-1.4.0/media_optimization_engine.egg-info/dependency_links.txt +1 -0
- media_optimization_engine-1.4.0/media_optimization_engine.egg-info/entry_points.txt +2 -0
- media_optimization_engine-1.4.0/media_optimization_engine.egg-info/requires.txt +32 -0
- media_optimization_engine-1.4.0/media_optimization_engine.egg-info/top_level.txt +1 -0
- media_optimization_engine-1.4.0/pyproject.toml +72 -0
- media_optimization_engine-1.4.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Achille Kabasele
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: media-optimization-engine
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Media Optimization Engineer for Django/Python: automatic model-field optimization, responsive picture/srcset rendering, AVIF/WebP derivatives, 360 media, autonomous nodes, and CDN-ready storage.
|
|
5
|
+
Author-email: Achille Kabasele <pepexykabasele@gmail.com>
|
|
6
|
+
Maintainer-email: Achille Kabasele <pepexykabasele@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/Kabasele754/media-optimization-engine
|
|
9
|
+
Project-URL: Repository, https://github.com/Kabasele754/media-optimization-engine
|
|
10
|
+
Project-URL: Issues, https://github.com/Kabasele754/media-optimization-engine/issues
|
|
11
|
+
Project-URL: Documentation, https://github.com/Kabasele754/media-optimization-engine/tree/master/docs
|
|
12
|
+
Keywords: django,media,image-optimization,avif,webp,panorama,360,marzipano,cdn,cloudflare-r2
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Classifier: Framework :: Django :: 4.2
|
|
16
|
+
Classifier: Framework :: Django :: 5.2
|
|
17
|
+
Classifier: Framework :: Django :: 6.0
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
25
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: Django<6.1,>=4.2.30
|
|
30
|
+
Requires-Dist: djangorestframework<4,>=3.14
|
|
31
|
+
Requires-Dist: Pillow<13,>=10.3
|
|
32
|
+
Requires-Dist: pillow-avif-plugin<2,>=1.5
|
|
33
|
+
Requires-Dist: numpy<3,>=1.26
|
|
34
|
+
Requires-Dist: blurhash<2,>=1.1
|
|
35
|
+
Provides-Extra: celery
|
|
36
|
+
Requires-Dist: celery<6,>=5.4; extra == "celery"
|
|
37
|
+
Requires-Dist: redis<7,>=5; extra == "celery"
|
|
38
|
+
Provides-Extra: postgres
|
|
39
|
+
Requires-Dist: psycopg[binary]<4,>=3.2; extra == "postgres"
|
|
40
|
+
Provides-Extra: s3
|
|
41
|
+
Requires-Dist: boto3<2,>=1.35; extra == "s3"
|
|
42
|
+
Requires-Dist: django-storages[s3]<2,>=1.14; extra == "s3"
|
|
43
|
+
Provides-Extra: r2
|
|
44
|
+
Requires-Dist: boto3<2,>=1.35; extra == "r2"
|
|
45
|
+
Requires-Dist: django-storages[s3]<2,>=1.14; extra == "r2"
|
|
46
|
+
Provides-Extra: observability
|
|
47
|
+
Requires-Dist: prometheus-client<1,>=0.21; extra == "observability"
|
|
48
|
+
Provides-Extra: all
|
|
49
|
+
Requires-Dist: celery<6,>=5.4; extra == "all"
|
|
50
|
+
Requires-Dist: redis<7,>=5; extra == "all"
|
|
51
|
+
Requires-Dist: psycopg[binary]<4,>=3.2; extra == "all"
|
|
52
|
+
Requires-Dist: boto3<2,>=1.35; extra == "all"
|
|
53
|
+
Requires-Dist: django-storages[s3]<2,>=1.14; extra == "all"
|
|
54
|
+
Requires-Dist: prometheus-client<1,>=0.21; extra == "all"
|
|
55
|
+
Dynamic: license-file
|
|
56
|
+
|
|
57
|
+
# Media Optimization Engineer 1.4
|
|
58
|
+
|
|
59
|
+
Media Optimization Engineer (MOE) is a reusable Django/Python package for image optimization, responsive delivery, and 360-degree media processing.
|
|
60
|
+
|
|
61
|
+
The public package is intentionally application-agnostic. It does not depend on any consuming project's themes, tenants, brands, business models, or private infrastructure.
|
|
62
|
+
|
|
63
|
+
## Core capabilities
|
|
64
|
+
|
|
65
|
+
### Standard images
|
|
66
|
+
|
|
67
|
+
- automatic ImageField / FileField integration;
|
|
68
|
+
- immutable originals;
|
|
69
|
+
- AVIF, WebP and JPEG derivatives;
|
|
70
|
+
- responsive width sets;
|
|
71
|
+
- semantic profiles such as avatar, card, hero, and default;
|
|
72
|
+
- focal-point aware crops;
|
|
73
|
+
- dominant color, BlurHash and lightweight placeholders;
|
|
74
|
+
- SHA-256 deduplication;
|
|
75
|
+
- local, S3-compatible and Cloudflare R2 storage;
|
|
76
|
+
- Redis/Celery processing when enabled;
|
|
77
|
+
- CDN-ready derivative URLs;
|
|
78
|
+
- Django template tags for responsive rendering.
|
|
79
|
+
|
|
80
|
+
### 360 panoramas
|
|
81
|
+
|
|
82
|
+
- equirectangular panorama support;
|
|
83
|
+
- progressive preview;
|
|
84
|
+
- multiresolution pyramids;
|
|
85
|
+
- tiled AVIF/WebP delivery;
|
|
86
|
+
- generic manifests;
|
|
87
|
+
- Pannellum, Marzipano and Three.js adapters;
|
|
88
|
+
- optional cubemap generation.
|
|
89
|
+
|
|
90
|
+
## Install
|
|
91
|
+
|
|
92
|
+
~~~bash
|
|
93
|
+
pip install media-optimization-engine
|
|
94
|
+
~~~
|
|
95
|
+
|
|
96
|
+
Optional infrastructure extras:
|
|
97
|
+
|
|
98
|
+
~~~bash
|
|
99
|
+
pip install "media-optimization-engine[all]"
|
|
100
|
+
~~~
|
|
101
|
+
|
|
102
|
+
## Django setup
|
|
103
|
+
|
|
104
|
+
~~~python
|
|
105
|
+
# settings.py
|
|
106
|
+
INSTALLED_APPS = [
|
|
107
|
+
# ...
|
|
108
|
+
"rest_framework",
|
|
109
|
+
"media_engine",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
MEDIA_ENGINE_TASK_MODE = "auto"
|
|
113
|
+
|
|
114
|
+
MEDIA_ENGINE_AUTO_FIELDS = {
|
|
115
|
+
"articles.Article": {
|
|
116
|
+
"cover_image": {
|
|
117
|
+
"profile": "hero",
|
|
118
|
+
"role": "hero",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
"accounts.Profile": {
|
|
122
|
+
"photo": {
|
|
123
|
+
"profile": "avatar",
|
|
124
|
+
"role": "content",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
"organizations.Organization": {
|
|
128
|
+
"logo": {
|
|
129
|
+
"profile": "default",
|
|
130
|
+
"role": "content",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
~~~
|
|
135
|
+
|
|
136
|
+
~~~python
|
|
137
|
+
# urls.py
|
|
138
|
+
from django.urls import include, path
|
|
139
|
+
|
|
140
|
+
urlpatterns = [
|
|
141
|
+
path("api/v1/", include("media_engine.urls")),
|
|
142
|
+
]
|
|
143
|
+
~~~
|
|
144
|
+
|
|
145
|
+
Then:
|
|
146
|
+
|
|
147
|
+
~~~bash
|
|
148
|
+
python manage.py migrate
|
|
149
|
+
python manage.py media_engine_doctor
|
|
150
|
+
~~~
|
|
151
|
+
|
|
152
|
+
## Django template rendering
|
|
153
|
+
|
|
154
|
+
For normal model fields, use the high-level media_image tag:
|
|
155
|
+
|
|
156
|
+
~~~django
|
|
157
|
+
{% load responsive_media %}
|
|
158
|
+
|
|
159
|
+
{% media_image article "cover_image"
|
|
160
|
+
alt=article.title
|
|
161
|
+
css_class="article-hero"
|
|
162
|
+
sizes="(max-width: 768px) 100vw, 1200px"
|
|
163
|
+
%}
|
|
164
|
+
~~~
|
|
165
|
+
|
|
166
|
+
MOE resolves the field's MediaBinding, generates a responsive picture with available AVIF/WebP sources, preserves the image's CSS classes, and falls back to the original field URL only when no processed binding is available.
|
|
167
|
+
|
|
168
|
+
### Logo or footer image
|
|
169
|
+
|
|
170
|
+
~~~django
|
|
171
|
+
{% load responsive_media %}
|
|
172
|
+
|
|
173
|
+
{% media_image organization "logo"
|
|
174
|
+
alt=organization.name
|
|
175
|
+
css_class="site-logo"
|
|
176
|
+
sizes="240px"
|
|
177
|
+
%}
|
|
178
|
+
~~~
|
|
179
|
+
|
|
180
|
+
### Card grid
|
|
181
|
+
|
|
182
|
+
~~~django
|
|
183
|
+
{% load responsive_media %}
|
|
184
|
+
|
|
185
|
+
{% for article in articles %}
|
|
186
|
+
<article class="article-card">
|
|
187
|
+
{% media_image article "cover_image"
|
|
188
|
+
alt=article.title
|
|
189
|
+
css_class="article-card__image"
|
|
190
|
+
sizes="(max-width: 640px) 92vw, (max-width: 1100px) 45vw, 360px"
|
|
191
|
+
%}
|
|
192
|
+
<h2>{{ article.title }}</h2>
|
|
193
|
+
</article>
|
|
194
|
+
{% endfor %}
|
|
195
|
+
~~~
|
|
196
|
+
|
|
197
|
+
### Direct binding rendering
|
|
198
|
+
|
|
199
|
+
Advanced integrations can render a known asset directly:
|
|
200
|
+
|
|
201
|
+
~~~django
|
|
202
|
+
{% load responsive_media %}
|
|
203
|
+
|
|
204
|
+
{% responsive_image binding.asset
|
|
205
|
+
profile=binding.profile
|
|
206
|
+
role=binding.role
|
|
207
|
+
alt=object.title
|
|
208
|
+
css_class="media-object"
|
|
209
|
+
picture_class="media-object-picture"
|
|
210
|
+
sizes="100vw"
|
|
211
|
+
%}
|
|
212
|
+
~~~
|
|
213
|
+
|
|
214
|
+
### One optimized URL
|
|
215
|
+
|
|
216
|
+
For Open Graph metadata, CSS backgrounds, emails, or APIs where picture is not possible:
|
|
217
|
+
|
|
218
|
+
~~~django
|
|
219
|
+
{% load responsive_media %}
|
|
220
|
+
|
|
221
|
+
{% media_url organization "logo" preferred_width=512 preferred_format="webp" as optimized_logo %}
|
|
222
|
+
|
|
223
|
+
<meta property="og:image" content="{{ optimized_logo }}">
|
|
224
|
+
~~~
|
|
225
|
+
|
|
226
|
+
For normal page images, prefer media_image over media_url; the browser can make a better final choice from srcset and sizes.
|
|
227
|
+
|
|
228
|
+
## Responsive selection
|
|
229
|
+
|
|
230
|
+
MOE provides width descriptors and sizes. The browser chooses the final resource using layout width, viewport, DPR, supported formats, and its own network heuristics.
|
|
231
|
+
|
|
232
|
+
JavaScript is not required for normal responsive images.
|
|
233
|
+
|
|
234
|
+
Runtime measurement is appropriate only for highly dynamic components whose rendered width cannot be described reliably with sizes, such as resizable editors, canvas-like builders, or asynchronously mounted panels.
|
|
235
|
+
|
|
236
|
+
## Backfill
|
|
237
|
+
|
|
238
|
+
Historical media can be ingested with maintenance commands:
|
|
239
|
+
|
|
240
|
+
~~~bash
|
|
241
|
+
python manage.py backfill_registered_media
|
|
242
|
+
python manage.py audit_media_engine --fail-on-incomplete
|
|
243
|
+
~~~
|
|
244
|
+
|
|
245
|
+
New uploads registered in MEDIA_ENGINE_AUTO_FIELDS are handled automatically.
|
|
246
|
+
|
|
247
|
+
## Runtime independence
|
|
248
|
+
|
|
249
|
+
Embedded Django mode and standalone-node mode are autonomous. Optional control-plane synchronization must never be required for upload, processing, rendering, or derivative delivery.
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
MIT License. Copyright (c) 2026 Achille Kabasele.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Media Optimization Engineer 1.4
|
|
2
|
+
|
|
3
|
+
Media Optimization Engineer (MOE) is a reusable Django/Python package for image optimization, responsive delivery, and 360-degree media processing.
|
|
4
|
+
|
|
5
|
+
The public package is intentionally application-agnostic. It does not depend on any consuming project's themes, tenants, brands, business models, or private infrastructure.
|
|
6
|
+
|
|
7
|
+
## Core capabilities
|
|
8
|
+
|
|
9
|
+
### Standard images
|
|
10
|
+
|
|
11
|
+
- automatic ImageField / FileField integration;
|
|
12
|
+
- immutable originals;
|
|
13
|
+
- AVIF, WebP and JPEG derivatives;
|
|
14
|
+
- responsive width sets;
|
|
15
|
+
- semantic profiles such as avatar, card, hero, and default;
|
|
16
|
+
- focal-point aware crops;
|
|
17
|
+
- dominant color, BlurHash and lightweight placeholders;
|
|
18
|
+
- SHA-256 deduplication;
|
|
19
|
+
- local, S3-compatible and Cloudflare R2 storage;
|
|
20
|
+
- Redis/Celery processing when enabled;
|
|
21
|
+
- CDN-ready derivative URLs;
|
|
22
|
+
- Django template tags for responsive rendering.
|
|
23
|
+
|
|
24
|
+
### 360 panoramas
|
|
25
|
+
|
|
26
|
+
- equirectangular panorama support;
|
|
27
|
+
- progressive preview;
|
|
28
|
+
- multiresolution pyramids;
|
|
29
|
+
- tiled AVIF/WebP delivery;
|
|
30
|
+
- generic manifests;
|
|
31
|
+
- Pannellum, Marzipano and Three.js adapters;
|
|
32
|
+
- optional cubemap generation.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
~~~bash
|
|
37
|
+
pip install media-optimization-engine
|
|
38
|
+
~~~
|
|
39
|
+
|
|
40
|
+
Optional infrastructure extras:
|
|
41
|
+
|
|
42
|
+
~~~bash
|
|
43
|
+
pip install "media-optimization-engine[all]"
|
|
44
|
+
~~~
|
|
45
|
+
|
|
46
|
+
## Django setup
|
|
47
|
+
|
|
48
|
+
~~~python
|
|
49
|
+
# settings.py
|
|
50
|
+
INSTALLED_APPS = [
|
|
51
|
+
# ...
|
|
52
|
+
"rest_framework",
|
|
53
|
+
"media_engine",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
MEDIA_ENGINE_TASK_MODE = "auto"
|
|
57
|
+
|
|
58
|
+
MEDIA_ENGINE_AUTO_FIELDS = {
|
|
59
|
+
"articles.Article": {
|
|
60
|
+
"cover_image": {
|
|
61
|
+
"profile": "hero",
|
|
62
|
+
"role": "hero",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
"accounts.Profile": {
|
|
66
|
+
"photo": {
|
|
67
|
+
"profile": "avatar",
|
|
68
|
+
"role": "content",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
"organizations.Organization": {
|
|
72
|
+
"logo": {
|
|
73
|
+
"profile": "default",
|
|
74
|
+
"role": "content",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
~~~
|
|
79
|
+
|
|
80
|
+
~~~python
|
|
81
|
+
# urls.py
|
|
82
|
+
from django.urls import include, path
|
|
83
|
+
|
|
84
|
+
urlpatterns = [
|
|
85
|
+
path("api/v1/", include("media_engine.urls")),
|
|
86
|
+
]
|
|
87
|
+
~~~
|
|
88
|
+
|
|
89
|
+
Then:
|
|
90
|
+
|
|
91
|
+
~~~bash
|
|
92
|
+
python manage.py migrate
|
|
93
|
+
python manage.py media_engine_doctor
|
|
94
|
+
~~~
|
|
95
|
+
|
|
96
|
+
## Django template rendering
|
|
97
|
+
|
|
98
|
+
For normal model fields, use the high-level media_image tag:
|
|
99
|
+
|
|
100
|
+
~~~django
|
|
101
|
+
{% load responsive_media %}
|
|
102
|
+
|
|
103
|
+
{% media_image article "cover_image"
|
|
104
|
+
alt=article.title
|
|
105
|
+
css_class="article-hero"
|
|
106
|
+
sizes="(max-width: 768px) 100vw, 1200px"
|
|
107
|
+
%}
|
|
108
|
+
~~~
|
|
109
|
+
|
|
110
|
+
MOE resolves the field's MediaBinding, generates a responsive picture with available AVIF/WebP sources, preserves the image's CSS classes, and falls back to the original field URL only when no processed binding is available.
|
|
111
|
+
|
|
112
|
+
### Logo or footer image
|
|
113
|
+
|
|
114
|
+
~~~django
|
|
115
|
+
{% load responsive_media %}
|
|
116
|
+
|
|
117
|
+
{% media_image organization "logo"
|
|
118
|
+
alt=organization.name
|
|
119
|
+
css_class="site-logo"
|
|
120
|
+
sizes="240px"
|
|
121
|
+
%}
|
|
122
|
+
~~~
|
|
123
|
+
|
|
124
|
+
### Card grid
|
|
125
|
+
|
|
126
|
+
~~~django
|
|
127
|
+
{% load responsive_media %}
|
|
128
|
+
|
|
129
|
+
{% for article in articles %}
|
|
130
|
+
<article class="article-card">
|
|
131
|
+
{% media_image article "cover_image"
|
|
132
|
+
alt=article.title
|
|
133
|
+
css_class="article-card__image"
|
|
134
|
+
sizes="(max-width: 640px) 92vw, (max-width: 1100px) 45vw, 360px"
|
|
135
|
+
%}
|
|
136
|
+
<h2>{{ article.title }}</h2>
|
|
137
|
+
</article>
|
|
138
|
+
{% endfor %}
|
|
139
|
+
~~~
|
|
140
|
+
|
|
141
|
+
### Direct binding rendering
|
|
142
|
+
|
|
143
|
+
Advanced integrations can render a known asset directly:
|
|
144
|
+
|
|
145
|
+
~~~django
|
|
146
|
+
{% load responsive_media %}
|
|
147
|
+
|
|
148
|
+
{% responsive_image binding.asset
|
|
149
|
+
profile=binding.profile
|
|
150
|
+
role=binding.role
|
|
151
|
+
alt=object.title
|
|
152
|
+
css_class="media-object"
|
|
153
|
+
picture_class="media-object-picture"
|
|
154
|
+
sizes="100vw"
|
|
155
|
+
%}
|
|
156
|
+
~~~
|
|
157
|
+
|
|
158
|
+
### One optimized URL
|
|
159
|
+
|
|
160
|
+
For Open Graph metadata, CSS backgrounds, emails, or APIs where picture is not possible:
|
|
161
|
+
|
|
162
|
+
~~~django
|
|
163
|
+
{% load responsive_media %}
|
|
164
|
+
|
|
165
|
+
{% media_url organization "logo" preferred_width=512 preferred_format="webp" as optimized_logo %}
|
|
166
|
+
|
|
167
|
+
<meta property="og:image" content="{{ optimized_logo }}">
|
|
168
|
+
~~~
|
|
169
|
+
|
|
170
|
+
For normal page images, prefer media_image over media_url; the browser can make a better final choice from srcset and sizes.
|
|
171
|
+
|
|
172
|
+
## Responsive selection
|
|
173
|
+
|
|
174
|
+
MOE provides width descriptors and sizes. The browser chooses the final resource using layout width, viewport, DPR, supported formats, and its own network heuristics.
|
|
175
|
+
|
|
176
|
+
JavaScript is not required for normal responsive images.
|
|
177
|
+
|
|
178
|
+
Runtime measurement is appropriate only for highly dynamic components whose rendered width cannot be described reliably with sizes, such as resizable editors, canvas-like builders, or asynchronously mounted panels.
|
|
179
|
+
|
|
180
|
+
## Backfill
|
|
181
|
+
|
|
182
|
+
Historical media can be ingested with maintenance commands:
|
|
183
|
+
|
|
184
|
+
~~~bash
|
|
185
|
+
python manage.py backfill_registered_media
|
|
186
|
+
python manage.py audit_media_engine --fail-on-incomplete
|
|
187
|
+
~~~
|
|
188
|
+
|
|
189
|
+
New uploads registered in MEDIA_ENGINE_AUTO_FIELDS are handled automatically.
|
|
190
|
+
|
|
191
|
+
## Runtime independence
|
|
192
|
+
|
|
193
|
+
Embedded Django mode and standalone-node mode are autonomous. Optional control-plane synchronization must never be required for upload, processing, rendering, or derivative delivery.
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT License. Copyright (c) 2026 Achille Kabasele.
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Flutter panorama adapter
|
|
2
|
+
|
|
3
|
+
`models.dart` parses the generic MOE panorama manifest. A Flutter viewer can select the lowest level that satisfies current viewport width and devicePixelRatio, then request only visible tiles.
|
|
4
|
+
|
|
5
|
+
Recommended policy:
|
|
6
|
+
- show `preview` immediately;
|
|
7
|
+
- select a level from viewport width x DPR;
|
|
8
|
+
- request only tiles intersecting the current field of view;
|
|
9
|
+
- keep adjacent tiles warm for smooth panning;
|
|
10
|
+
- use AVIF where the Flutter image stack supports it, otherwise WebP.
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
class PanoramaTile {
|
|
2
|
+
final int level;
|
|
3
|
+
final int x;
|
|
4
|
+
final int y;
|
|
5
|
+
final int width;
|
|
6
|
+
final int height;
|
|
7
|
+
final String url;
|
|
8
|
+
|
|
9
|
+
const PanoramaTile({
|
|
10
|
+
required this.level,
|
|
11
|
+
required this.x,
|
|
12
|
+
required this.y,
|
|
13
|
+
required this.width,
|
|
14
|
+
required this.height,
|
|
15
|
+
required this.url,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
factory PanoramaTile.fromJson(int level, Map<String, dynamic> json) => PanoramaTile(
|
|
19
|
+
level: level,
|
|
20
|
+
x: json['x'] as int,
|
|
21
|
+
y: json['y'] as int,
|
|
22
|
+
width: json['width'] as int,
|
|
23
|
+
height: json['height'] as int,
|
|
24
|
+
url: json['url'] as String,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class PanoramaLevel {
|
|
29
|
+
final int level;
|
|
30
|
+
final int width;
|
|
31
|
+
final int height;
|
|
32
|
+
final int cols;
|
|
33
|
+
final int rows;
|
|
34
|
+
final int tileSize;
|
|
35
|
+
final Map<String, List<PanoramaTile>> formats;
|
|
36
|
+
|
|
37
|
+
const PanoramaLevel({
|
|
38
|
+
required this.level,
|
|
39
|
+
required this.width,
|
|
40
|
+
required this.height,
|
|
41
|
+
required this.cols,
|
|
42
|
+
required this.rows,
|
|
43
|
+
required this.tileSize,
|
|
44
|
+
required this.formats,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
factory PanoramaLevel.fromJson(Map<String, dynamic> json) {
|
|
48
|
+
final level = json['level'] as int;
|
|
49
|
+
final rawFormats = Map<String, dynamic>.from(json['formats'] as Map);
|
|
50
|
+
return PanoramaLevel(
|
|
51
|
+
level: level,
|
|
52
|
+
width: json['width'] as int,
|
|
53
|
+
height: json['height'] as int,
|
|
54
|
+
cols: json['cols'] as int,
|
|
55
|
+
rows: json['rows'] as int,
|
|
56
|
+
tileSize: json['tile_size'] as int,
|
|
57
|
+
formats: rawFormats.map((key, value) => MapEntry(
|
|
58
|
+
key,
|
|
59
|
+
(value as List)
|
|
60
|
+
.map((e) => PanoramaTile.fromJson(level, Map<String, dynamic>.from(e as Map)))
|
|
61
|
+
.toList(),
|
|
62
|
+
)),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class PanoramaManifest {
|
|
68
|
+
final String assetId;
|
|
69
|
+
final String projection;
|
|
70
|
+
final String preview;
|
|
71
|
+
final List<PanoramaLevel> levels;
|
|
72
|
+
|
|
73
|
+
const PanoramaManifest({
|
|
74
|
+
required this.assetId,
|
|
75
|
+
required this.projection,
|
|
76
|
+
required this.preview,
|
|
77
|
+
required this.levels,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
factory PanoramaManifest.fromJson(Map<String, dynamic> json) => PanoramaManifest(
|
|
81
|
+
assetId: json['asset_id'] as String,
|
|
82
|
+
projection: json['projection'] as String? ?? 'equirectangular',
|
|
83
|
+
preview: json['preview'] as String? ?? '',
|
|
84
|
+
levels: (json['levels'] as List? ?? const [])
|
|
85
|
+
.map((e) => PanoramaLevel.fromJson(Map<String, dynamic>.from(e as Map)))
|
|
86
|
+
.toList(),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def to_marzipano(manifest: dict) -> dict:
|
|
5
|
+
"""Return a browser-friendly Marzipano description.
|
|
6
|
+
|
|
7
|
+
For ``panorama.marzipano`` the engine emits a true multires CubeGeometry
|
|
8
|
+
pyramid. Tiles stay explicit so this works with local storage, S3 and R2
|
|
9
|
+
without assuming a specific URL layout.
|
|
10
|
+
"""
|
|
11
|
+
layout = manifest.get('layout', '')
|
|
12
|
+
if layout != 'cube-multires':
|
|
13
|
+
# Graceful preview fallback for a generic equirectangular pipeline.
|
|
14
|
+
return {
|
|
15
|
+
'type': 'equirectangular',
|
|
16
|
+
'preview': manifest.get('preview', ''),
|
|
17
|
+
'projection': manifest.get('projection', 'equirectangular'),
|
|
18
|
+
'levels': manifest.get('levels', []),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
levels = []
|
|
22
|
+
tiles = []
|
|
23
|
+
preferred_format = 'webp'
|
|
24
|
+
for item in manifest.get('levels', []):
|
|
25
|
+
formats = item.get('formats', {})
|
|
26
|
+
selected = formats.get('webp') or formats.get('avif') or next(iter(formats.values()), [])
|
|
27
|
+
if not selected:
|
|
28
|
+
continue
|
|
29
|
+
if formats.get('webp') is None and formats.get('avif'):
|
|
30
|
+
preferred_format = 'avif'
|
|
31
|
+
levels.append({
|
|
32
|
+
'level': item['level'],
|
|
33
|
+
'size': item.get('size') or item.get('width'),
|
|
34
|
+
'tileSize': item.get('tile_size', 512),
|
|
35
|
+
'fallbackOnly': bool(item.get('fallback_only', False)),
|
|
36
|
+
})
|
|
37
|
+
for tile in selected:
|
|
38
|
+
tiles.append({
|
|
39
|
+
'z': item['level'],
|
|
40
|
+
'face': tile.get('face', ''),
|
|
41
|
+
'x': tile['x'],
|
|
42
|
+
'y': tile['y'],
|
|
43
|
+
'url': tile['url'],
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
'type': 'cube-multires',
|
|
48
|
+
'preview': manifest.get('preview', ''),
|
|
49
|
+
'projection': manifest.get('projection', 'equirectangular'),
|
|
50
|
+
'format': preferred_format,
|
|
51
|
+
'geometry': {'type': 'cube', 'levels': levels},
|
|
52
|
+
'tiles': tiles,
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def to_pannellum(manifest: dict) -> dict:
|
|
5
|
+
levels = manifest.get('levels', [])
|
|
6
|
+
if not levels:
|
|
7
|
+
return {'type': 'equirectangular', 'panorama': manifest.get('preview', '')}
|
|
8
|
+
max_level = max(level['level'] for level in levels)
|
|
9
|
+
tile_size = levels[0].get('tile_size', 512)
|
|
10
|
+
base = {
|
|
11
|
+
'type': 'multires',
|
|
12
|
+
'multiRes': {
|
|
13
|
+
'basePath': '',
|
|
14
|
+
'path': '{z}/{x}_{y}.webp',
|
|
15
|
+
'fallbackPath': '{z}/{x}_{y}.webp',
|
|
16
|
+
'extension': 'webp',
|
|
17
|
+
'tileResolution': tile_size,
|
|
18
|
+
'maxLevel': max_level,
|
|
19
|
+
'cubeResolution': levels[-1].get('width', 0),
|
|
20
|
+
},
|
|
21
|
+
'preview': manifest.get('preview', ''),
|
|
22
|
+
}
|
|
23
|
+
return base
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def to_threejs(manifest: dict) -> dict:
|
|
5
|
+
return {
|
|
6
|
+
'type': 'equirectangular-multires',
|
|
7
|
+
'preview': manifest.get('preview', ''),
|
|
8
|
+
'levels': manifest.get('levels', []),
|
|
9
|
+
'projection': manifest.get('projection', 'equirectangular'),
|
|
10
|
+
}
|