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.
Files changed (90) hide show
  1. media_optimization_engine-1.4.0/LICENSE +21 -0
  2. media_optimization_engine-1.4.0/PKG-INFO +253 -0
  3. media_optimization_engine-1.4.0/README.md +197 -0
  4. media_optimization_engine-1.4.0/media_engine/__init__.py +7 -0
  5. media_optimization_engine-1.4.0/media_engine/adapters/__init__.py +0 -0
  6. media_optimization_engine-1.4.0/media_engine/adapters/flutter/README.md +10 -0
  7. media_optimization_engine-1.4.0/media_engine/adapters/flutter/__init__.py +0 -0
  8. media_optimization_engine-1.4.0/media_engine/adapters/flutter/models.dart +88 -0
  9. media_optimization_engine-1.4.0/media_engine/adapters/marzipano/__init__.py +2 -0
  10. media_optimization_engine-1.4.0/media_engine/adapters/marzipano/manifest.py +53 -0
  11. media_optimization_engine-1.4.0/media_engine/adapters/pannellum/__init__.py +2 -0
  12. media_optimization_engine-1.4.0/media_engine/adapters/pannellum/manifest.py +23 -0
  13. media_optimization_engine-1.4.0/media_engine/adapters/threejs/__init__.py +2 -0
  14. media_optimization_engine-1.4.0/media_engine/adapters/threejs/manifest.py +10 -0
  15. media_optimization_engine-1.4.0/media_engine/admin.py +36 -0
  16. media_optimization_engine-1.4.0/media_engine/apps.py +11 -0
  17. media_optimization_engine-1.4.0/media_engine/auth.py +33 -0
  18. media_optimization_engine-1.4.0/media_engine/autoregister.py +108 -0
  19. media_optimization_engine-1.4.0/media_engine/control_plane.py +274 -0
  20. media_optimization_engine-1.4.0/media_engine/doctor.py +177 -0
  21. media_optimization_engine-1.4.0/media_engine/focal_detectors.py +28 -0
  22. media_optimization_engine-1.4.0/media_engine/health.py +78 -0
  23. media_optimization_engine-1.4.0/media_engine/image_ops.py +142 -0
  24. media_optimization_engine-1.4.0/media_engine/integration.py +34 -0
  25. media_optimization_engine-1.4.0/media_engine/locks.py +20 -0
  26. media_optimization_engine-1.4.0/media_engine/management/__init__.py +0 -0
  27. media_optimization_engine-1.4.0/media_engine/management/commands/__init__.py +0 -0
  28. media_optimization_engine-1.4.0/media_engine/management/commands/audit_media_engine.py +54 -0
  29. media_optimization_engine-1.4.0/media_engine/management/commands/audit_panorama_media.py +20 -0
  30. media_optimization_engine-1.4.0/media_engine/management/commands/backfill_panorama_media.py +32 -0
  31. media_optimization_engine-1.4.0/media_engine/management/commands/backfill_registered_media.py +21 -0
  32. media_optimization_engine-1.4.0/media_engine/management/commands/backfill_responsive_images.py +27 -0
  33. media_optimization_engine-1.4.0/media_engine/management/commands/cleanup_image_derivatives.py +30 -0
  34. media_optimization_engine-1.4.0/media_engine/management/commands/media_engine_doctor.py +15 -0
  35. media_optimization_engine-1.4.0/media_engine/management/commands/media_engine_node_report.py +24 -0
  36. media_optimization_engine-1.4.0/media_engine/management/commands/rollback_media_engine_config.py +14 -0
  37. media_optimization_engine-1.4.0/media_engine/management/commands/sync_media_engine_hub.py +20 -0
  38. media_optimization_engine-1.4.0/media_engine/manifest.py +89 -0
  39. media_optimization_engine-1.4.0/media_engine/metrics.py +34 -0
  40. media_optimization_engine-1.4.0/media_engine/migrations/0001_initial.py +87 -0
  41. media_optimization_engine-1.4.0/media_engine/migrations/0002_panorama_multires.py +44 -0
  42. media_optimization_engine-1.4.0/media_engine/migrations/0003_panorama_cube_tiles.py +31 -0
  43. media_optimization_engine-1.4.0/media_engine/migrations/__init__.py +0 -0
  44. media_optimization_engine-1.4.0/media_engine/models.py +145 -0
  45. media_optimization_engine-1.4.0/media_engine/node.py +29 -0
  46. media_optimization_engine-1.4.0/media_engine/processors/__init__.py +0 -0
  47. media_optimization_engine-1.4.0/media_engine/processors/base.py +21 -0
  48. media_optimization_engine-1.4.0/media_engine/processors/image/__init__.py +0 -0
  49. media_optimization_engine-1.4.0/media_engine/processors/image/standard.py +16 -0
  50. media_optimization_engine-1.4.0/media_engine/processors/panorama/__init__.py +5 -0
  51. media_optimization_engine-1.4.0/media_engine/processors/panorama/cube_tiles.py +121 -0
  52. media_optimization_engine-1.4.0/media_engine/processors/panorama/cubemap.py +40 -0
  53. media_optimization_engine-1.4.0/media_engine/processors/panorama/detect.py +23 -0
  54. media_optimization_engine-1.4.0/media_engine/processors/panorama/equirectangular.py +149 -0
  55. media_optimization_engine-1.4.0/media_engine/processors/panorama/manifest.py +105 -0
  56. media_optimization_engine-1.4.0/media_engine/processors/panorama/tiles.py +88 -0
  57. media_optimization_engine-1.4.0/media_engine/processors/registry.py +17 -0
  58. media_optimization_engine-1.4.0/media_engine/profiles.py +77 -0
  59. media_optimization_engine-1.4.0/media_engine/queueing.py +34 -0
  60. media_optimization_engine-1.4.0/media_engine/registry.py +49 -0
  61. media_optimization_engine-1.4.0/media_engine/runtime_config.py +37 -0
  62. media_optimization_engine-1.4.0/media_engine/serializers.py +29 -0
  63. media_optimization_engine-1.4.0/media_engine/services.py +187 -0
  64. media_optimization_engine-1.4.0/media_engine/signals.py +5 -0
  65. media_optimization_engine-1.4.0/media_engine/spec/openapi.yaml +191 -0
  66. media_optimization_engine-1.4.0/media_engine/static/media_engine/adaptive-media.js +77 -0
  67. media_optimization_engine-1.4.0/media_engine/static/media_engine/media-cache-sw.js +17 -0
  68. media_optimization_engine-1.4.0/media_engine/storage_paths.py +13 -0
  69. media_optimization_engine-1.4.0/media_engine/system_views.py +102 -0
  70. media_optimization_engine-1.4.0/media_engine/tasks.py +44 -0
  71. media_optimization_engine-1.4.0/media_engine/templatetags/__init__.py +0 -0
  72. media_optimization_engine-1.4.0/media_engine/templatetags/responsive_media.py +262 -0
  73. media_optimization_engine-1.4.0/media_engine/tests/__init__.py +0 -0
  74. media_optimization_engine-1.4.0/media_engine/tests/test_distributed_node.py +72 -0
  75. media_optimization_engine-1.4.0/media_engine/tests/test_image_ops.py +14 -0
  76. media_optimization_engine-1.4.0/media_engine/tests/test_panorama.py +31 -0
  77. media_optimization_engine-1.4.0/media_engine/tests/test_paths.py +9 -0
  78. media_optimization_engine-1.4.0/media_engine/tests/test_profiles.py +8 -0
  79. media_optimization_engine-1.4.0/media_engine/tests/test_responsive_media.py +153 -0
  80. media_optimization_engine-1.4.0/media_engine/urls.py +14 -0
  81. media_optimization_engine-1.4.0/media_engine/validators.py +42 -0
  82. media_optimization_engine-1.4.0/media_engine/views.py +111 -0
  83. media_optimization_engine-1.4.0/media_optimization_engine.egg-info/PKG-INFO +253 -0
  84. media_optimization_engine-1.4.0/media_optimization_engine.egg-info/SOURCES.txt +88 -0
  85. media_optimization_engine-1.4.0/media_optimization_engine.egg-info/dependency_links.txt +1 -0
  86. media_optimization_engine-1.4.0/media_optimization_engine.egg-info/entry_points.txt +2 -0
  87. media_optimization_engine-1.4.0/media_optimization_engine.egg-info/requires.txt +32 -0
  88. media_optimization_engine-1.4.0/media_optimization_engine.egg-info/top_level.txt +1 -0
  89. media_optimization_engine-1.4.0/pyproject.toml +72 -0
  90. 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.
@@ -0,0 +1,7 @@
1
+ __version__ = '1.4.0rc3'
2
+
3
+ def register_model_image(*args, **kwargs):
4
+ from .registry import register_model_image as _register
5
+ return _register(*args, **kwargs)
6
+
7
+ default_app_config = 'media_engine.apps.MediaEngineConfig'
@@ -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.
@@ -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,2 @@
1
+ from .manifest import to_marzipano
2
+ __all__ = ['to_marzipano']
@@ -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,2 @@
1
+ from .manifest import to_pannellum
2
+ __all__ = ['to_pannellum']
@@ -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,2 @@
1
+ from .manifest import to_threejs
2
+ __all__ = ['to_threejs']
@@ -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
+ }