media-optimization-engine 1.4.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.
- media_engine/__init__.py +7 -0
- media_engine/adapters/__init__.py +0 -0
- media_engine/adapters/flutter/README.md +10 -0
- media_engine/adapters/flutter/__init__.py +0 -0
- media_engine/adapters/flutter/models.dart +88 -0
- media_engine/adapters/marzipano/__init__.py +2 -0
- media_engine/adapters/marzipano/manifest.py +53 -0
- media_engine/adapters/pannellum/__init__.py +2 -0
- media_engine/adapters/pannellum/manifest.py +23 -0
- media_engine/adapters/threejs/__init__.py +2 -0
- media_engine/adapters/threejs/manifest.py +10 -0
- media_engine/admin.py +36 -0
- media_engine/apps.py +11 -0
- media_engine/auth.py +33 -0
- media_engine/autoregister.py +108 -0
- media_engine/control_plane.py +274 -0
- media_engine/doctor.py +177 -0
- media_engine/focal_detectors.py +28 -0
- media_engine/health.py +78 -0
- media_engine/image_ops.py +142 -0
- media_engine/integration.py +34 -0
- media_engine/locks.py +20 -0
- media_engine/management/__init__.py +0 -0
- media_engine/management/commands/__init__.py +0 -0
- media_engine/management/commands/audit_media_engine.py +54 -0
- media_engine/management/commands/audit_panorama_media.py +20 -0
- media_engine/management/commands/backfill_panorama_media.py +32 -0
- media_engine/management/commands/backfill_registered_media.py +21 -0
- media_engine/management/commands/backfill_responsive_images.py +27 -0
- media_engine/management/commands/cleanup_image_derivatives.py +30 -0
- media_engine/management/commands/media_engine_doctor.py +15 -0
- media_engine/management/commands/media_engine_node_report.py +24 -0
- media_engine/management/commands/rollback_media_engine_config.py +14 -0
- media_engine/management/commands/sync_media_engine_hub.py +20 -0
- media_engine/manifest.py +89 -0
- media_engine/metrics.py +34 -0
- media_engine/migrations/0001_initial.py +87 -0
- media_engine/migrations/0002_panorama_multires.py +44 -0
- media_engine/migrations/0003_panorama_cube_tiles.py +31 -0
- media_engine/migrations/__init__.py +0 -0
- media_engine/models.py +145 -0
- media_engine/node.py +29 -0
- media_engine/processors/__init__.py +0 -0
- media_engine/processors/base.py +21 -0
- media_engine/processors/image/__init__.py +0 -0
- media_engine/processors/image/standard.py +16 -0
- media_engine/processors/panorama/__init__.py +5 -0
- media_engine/processors/panorama/cube_tiles.py +121 -0
- media_engine/processors/panorama/cubemap.py +40 -0
- media_engine/processors/panorama/detect.py +23 -0
- media_engine/processors/panorama/equirectangular.py +149 -0
- media_engine/processors/panorama/manifest.py +105 -0
- media_engine/processors/panorama/tiles.py +88 -0
- media_engine/processors/registry.py +17 -0
- media_engine/profiles.py +77 -0
- media_engine/queueing.py +34 -0
- media_engine/registry.py +49 -0
- media_engine/runtime_config.py +37 -0
- media_engine/serializers.py +29 -0
- media_engine/services.py +187 -0
- media_engine/signals.py +5 -0
- media_engine/spec/openapi.yaml +191 -0
- media_engine/static/media_engine/adaptive-media.js +77 -0
- media_engine/static/media_engine/media-cache-sw.js +17 -0
- media_engine/storage_paths.py +13 -0
- media_engine/system_views.py +102 -0
- media_engine/tasks.py +44 -0
- media_engine/templatetags/__init__.py +0 -0
- media_engine/templatetags/responsive_media.py +262 -0
- media_engine/tests/__init__.py +0 -0
- media_engine/tests/test_distributed_node.py +72 -0
- media_engine/tests/test_image_ops.py +14 -0
- media_engine/tests/test_panorama.py +31 -0
- media_engine/tests/test_paths.py +9 -0
- media_engine/tests/test_profiles.py +8 -0
- media_engine/tests/test_responsive_media.py +153 -0
- media_engine/urls.py +14 -0
- media_engine/validators.py +42 -0
- media_engine/views.py +111 -0
- media_optimization_engine-1.4.0.dist-info/METADATA +253 -0
- media_optimization_engine-1.4.0.dist-info/RECORD +85 -0
- media_optimization_engine-1.4.0.dist-info/WHEEL +5 -0
- media_optimization_engine-1.4.0.dist-info/entry_points.txt +2 -0
- media_optimization_engine-1.4.0.dist-info/licenses/LICENSE +21 -0
- media_optimization_engine-1.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
from django.test import SimpleTestCase
|
|
5
|
+
|
|
6
|
+
from media_engine.templatetags.responsive_media import media_image, media_url, responsive_image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResponsiveImageTagTests(SimpleTestCase):
|
|
10
|
+
def manifest(self):
|
|
11
|
+
return {
|
|
12
|
+
'width': 1600,
|
|
13
|
+
'height': 900,
|
|
14
|
+
'dominant_color': '#123456',
|
|
15
|
+
'original_url': '/media/original.png',
|
|
16
|
+
'fallback': {'url': '/media/fallback.webp', 'width': 1280, 'height': 720},
|
|
17
|
+
'variants': {
|
|
18
|
+
'avif': [
|
|
19
|
+
{'url': '/media/480.avif', 'width': 480, 'height': 270},
|
|
20
|
+
{'url': '/media/960.avif', 'width': 960, 'height': 540},
|
|
21
|
+
],
|
|
22
|
+
'webp': [
|
|
23
|
+
{'url': '/media/480.webp', 'width': 480, 'height': 270},
|
|
24
|
+
{'url': '/media/960.webp', 'width': 960, 'height': 540},
|
|
25
|
+
{'url': '/media/1280.webp', 'width': 1280, 'height': 720},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@patch('media_engine.templatetags.responsive_media.build_manifest')
|
|
31
|
+
def test_keeps_image_presentation_attributes_on_nested_img(self, build_manifest):
|
|
32
|
+
build_manifest.return_value = self.manifest()
|
|
33
|
+
|
|
34
|
+
html = str(
|
|
35
|
+
responsive_image(
|
|
36
|
+
object(),
|
|
37
|
+
css_class='site-logo rounded',
|
|
38
|
+
picture_class='media-picture site-logo-wrap',
|
|
39
|
+
style='object-fit:contain;width:100%',
|
|
40
|
+
element_id='brand-logo',
|
|
41
|
+
sizes='(max-width: 600px) 50vw, 240px',
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self.assertIn('<picture class="media-picture site-logo-wrap">', html)
|
|
46
|
+
self.assertIn('class="site-logo rounded"', html)
|
|
47
|
+
self.assertIn('id="brand-logo"', html)
|
|
48
|
+
self.assertIn('style="background:#123456;object-fit:contain;width:100%"', html)
|
|
49
|
+
self.assertIn('srcset="/media/480.avif 480w, /media/960.avif 960w"', html)
|
|
50
|
+
self.assertIn('srcset="/media/480.webp 480w, /media/960.webp 960w, /media/1280.webp 1280w"', html)
|
|
51
|
+
self.assertIn('src="/media/fallback.webp"', html)
|
|
52
|
+
|
|
53
|
+
@patch('media_engine.templatetags.responsive_media.build_manifest')
|
|
54
|
+
def test_adaptive_mode_marks_picture_for_runtime_sizing(self, build_manifest):
|
|
55
|
+
build_manifest.return_value = self.manifest()
|
|
56
|
+
|
|
57
|
+
html = str(responsive_image(object(), adaptive=True))
|
|
58
|
+
|
|
59
|
+
self.assertIn('data-moe-adaptive="1"', html)
|
|
60
|
+
|
|
61
|
+
@patch('media_engine.templatetags.responsive_media.build_manifest')
|
|
62
|
+
def test_custom_loading_and_dimensions_override_defaults(self, build_manifest):
|
|
63
|
+
build_manifest.return_value = self.manifest()
|
|
64
|
+
|
|
65
|
+
html = str(
|
|
66
|
+
responsive_image(
|
|
67
|
+
object(),
|
|
68
|
+
role='hero',
|
|
69
|
+
width='320',
|
|
70
|
+
height='180',
|
|
71
|
+
loading='lazy',
|
|
72
|
+
decoding='sync',
|
|
73
|
+
fetchpriority='low',
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.assertIn('width="320"', html)
|
|
78
|
+
self.assertIn('height="180"', html)
|
|
79
|
+
self.assertIn('loading="lazy"', html)
|
|
80
|
+
self.assertIn('decoding="sync"', html)
|
|
81
|
+
self.assertIn('fetchpriority="low"', html)
|
|
82
|
+
|
|
83
|
+
@patch('media_engine.templatetags.responsive_media.build_manifest')
|
|
84
|
+
def test_existing_call_signature_remains_compatible(self, build_manifest):
|
|
85
|
+
build_manifest.return_value = self.manifest()
|
|
86
|
+
|
|
87
|
+
html = str(responsive_image(object(), profile='card', role='content', alt='Card'))
|
|
88
|
+
|
|
89
|
+
self.assertTrue(html.startswith('<picture>'))
|
|
90
|
+
self.assertIn('alt="Card"', html)
|
|
91
|
+
self.assertIn('loading="lazy"', html)
|
|
92
|
+
self.assertIn('width="1280"', html)
|
|
93
|
+
self.assertIn('height="720"', html)
|
|
94
|
+
|
|
95
|
+
@patch('media_engine.templatetags.responsive_media._binding_for')
|
|
96
|
+
@patch('media_engine.templatetags.responsive_media.build_manifest')
|
|
97
|
+
def test_media_image_resolves_model_field_binding(self, build_manifest, binding_for):
|
|
98
|
+
build_manifest.return_value = self.manifest()
|
|
99
|
+
binding_for.return_value = SimpleNamespace(
|
|
100
|
+
asset=object(),
|
|
101
|
+
profile='hero',
|
|
102
|
+
role='hero',
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
html = str(
|
|
106
|
+
media_image(
|
|
107
|
+
SimpleNamespace(pk=7),
|
|
108
|
+
'cover_image',
|
|
109
|
+
alt='Article cover',
|
|
110
|
+
css_class='article-hero',
|
|
111
|
+
sizes='(max-width: 768px) 100vw, 1200px',
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self.assertIn('<picture', html)
|
|
116
|
+
self.assertIn('class="article-hero"', html)
|
|
117
|
+
self.assertIn('loading="eager"', html)
|
|
118
|
+
self.assertIn('fetchpriority="high"', html)
|
|
119
|
+
self.assertIn('/media/960.avif 960w', html)
|
|
120
|
+
|
|
121
|
+
@patch('media_engine.templatetags.responsive_media._field_url')
|
|
122
|
+
@patch('media_engine.templatetags.responsive_media._binding_for')
|
|
123
|
+
def test_media_image_falls_back_to_field_url_without_binding(self, binding_for, field_url):
|
|
124
|
+
binding_for.return_value = None
|
|
125
|
+
field_url.return_value = '/uploads/photo.jpg'
|
|
126
|
+
|
|
127
|
+
html = str(
|
|
128
|
+
media_image(
|
|
129
|
+
SimpleNamespace(pk=9),
|
|
130
|
+
'photo',
|
|
131
|
+
alt='Profile',
|
|
132
|
+
css_class='avatar',
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self.assertTrue(html.startswith('<img'))
|
|
137
|
+
self.assertIn('src="/uploads/photo.jpg"', html)
|
|
138
|
+
self.assertIn('loading="lazy"', html)
|
|
139
|
+
|
|
140
|
+
@patch('media_engine.templatetags.responsive_media._binding_for')
|
|
141
|
+
@patch('media_engine.templatetags.responsive_media.build_manifest')
|
|
142
|
+
def test_media_url_selects_smallest_variant_at_or_above_target(self, build_manifest, binding_for):
|
|
143
|
+
build_manifest.return_value = self.manifest()
|
|
144
|
+
binding_for.return_value = SimpleNamespace(asset=object(), profile='default')
|
|
145
|
+
|
|
146
|
+
url = media_url(
|
|
147
|
+
SimpleNamespace(pk=3),
|
|
148
|
+
'logo',
|
|
149
|
+
preferred_width=700,
|
|
150
|
+
preferred_format='webp',
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
self.assertEqual(url, '/media/960.webp')
|
media_engine/urls.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
from rest_framework.routers import DefaultRouter
|
|
3
|
+
from .views import MediaViewSet
|
|
4
|
+
from . import system_views
|
|
5
|
+
|
|
6
|
+
router = DefaultRouter()
|
|
7
|
+
router.register('images', MediaViewSet, basename='media-image')
|
|
8
|
+
|
|
9
|
+
urlpatterns = [
|
|
10
|
+
path('system/capabilities/', system_views.capabilities, name='media-engine-capabilities'),
|
|
11
|
+
path('system/health/', system_views.health, name='media-engine-health'),
|
|
12
|
+
path('system/version/', system_views.version, name='media-engine-version'),
|
|
13
|
+
path('system/openapi.yaml', system_views.openapi_yaml, name='media-engine-openapi'),
|
|
14
|
+
] + router.urls
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.core.exceptions import ValidationError
|
|
4
|
+
from PIL import Image
|
|
5
|
+
try:
|
|
6
|
+
import pillow_avif # noqa: F401
|
|
7
|
+
except Exception:
|
|
8
|
+
pillow_avif = None
|
|
9
|
+
|
|
10
|
+
ALLOWED_FORMATS = {'JPEG', 'PNG', 'WEBP', 'AVIF'}
|
|
11
|
+
MIME_BY_FORMAT = {
|
|
12
|
+
'JPEG': 'image/jpeg',
|
|
13
|
+
'PNG': 'image/png',
|
|
14
|
+
'WEBP': 'image/webp',
|
|
15
|
+
'AVIF': 'image/avif',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def inspect_upload(uploaded_file):
|
|
20
|
+
max_bytes = getattr(settings, 'MEDIA_ENGINE_MAX_UPLOAD_BYTES', 25 * 1024 * 1024)
|
|
21
|
+
if uploaded_file.size > max_bytes:
|
|
22
|
+
raise ValidationError(f'Image exceeds maximum upload size of {max_bytes} bytes.')
|
|
23
|
+
|
|
24
|
+
uploaded_file.seek(0)
|
|
25
|
+
raw = uploaded_file.read()
|
|
26
|
+
uploaded_file.seek(0)
|
|
27
|
+
try:
|
|
28
|
+
image = Image.open(io.BytesIO(raw))
|
|
29
|
+
image.verify()
|
|
30
|
+
image = Image.open(io.BytesIO(raw))
|
|
31
|
+
fmt = (image.format or '').upper()
|
|
32
|
+
if fmt not in ALLOWED_FORMATS:
|
|
33
|
+
raise ValidationError(f'Unsupported image format: {fmt or "unknown"}')
|
|
34
|
+
width, height = image.size
|
|
35
|
+
max_pixels = getattr(settings, 'MEDIA_ENGINE_MAX_PIXELS', 50_000_000)
|
|
36
|
+
if width * height > max_pixels:
|
|
37
|
+
raise ValidationError(f'Image exceeds maximum pixel count of {max_pixels}.')
|
|
38
|
+
except ValidationError:
|
|
39
|
+
raise
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
raise ValidationError('Invalid or corrupted image.') from exc
|
|
42
|
+
return raw, fmt, MIME_BY_FORMAT[fmt], width, height
|
media_engine/views.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.http import HttpResponseRedirect
|
|
3
|
+
from django.shortcuts import get_object_or_404
|
|
4
|
+
from rest_framework import status
|
|
5
|
+
from rest_framework.decorators import action
|
|
6
|
+
from rest_framework.response import Response
|
|
7
|
+
from rest_framework.viewsets import ViewSet
|
|
8
|
+
from .manifest import build_manifest
|
|
9
|
+
from .models import MediaAsset, MediaVariant
|
|
10
|
+
from .profiles import get_profile
|
|
11
|
+
from .serializers import MediaUploadSerializer, MediaAssetSerializer
|
|
12
|
+
from .services import ingest_uploaded_file, generate_variant
|
|
13
|
+
from .queueing import enqueue_asset
|
|
14
|
+
from .auth import MediaEngineApiKeyPermission
|
|
15
|
+
from .node import scoped_owner_ref
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MediaViewSet(ViewSet):
|
|
19
|
+
permission_classes = [MediaEngineApiKeyPermission]
|
|
20
|
+
|
|
21
|
+
def create(self, request):
|
|
22
|
+
serializer = MediaUploadSerializer(data=request.data)
|
|
23
|
+
serializer.is_valid(raise_exception=True)
|
|
24
|
+
data = serializer.validated_data
|
|
25
|
+
asset, created = ingest_uploaded_file(
|
|
26
|
+
data['file'],
|
|
27
|
+
owner_ref=scoped_owner_ref(data.get('owner_ref', '')),
|
|
28
|
+
focal_x=data.get('focal_x'),
|
|
29
|
+
focal_y=data.get('focal_y'),
|
|
30
|
+
profile=data.get('profile', 'default'),
|
|
31
|
+
media_kind=data.get('media_kind', ''),
|
|
32
|
+
projection=data.get('projection', ''),
|
|
33
|
+
enqueue=True,
|
|
34
|
+
)
|
|
35
|
+
output = MediaAssetSerializer(asset, context={'profile': data.get('profile', 'default')}).data
|
|
36
|
+
return Response(output, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
|
|
37
|
+
|
|
38
|
+
def retrieve(self, request, pk=None):
|
|
39
|
+
asset = get_object_or_404(MediaAsset, pk=pk)
|
|
40
|
+
profile = request.query_params.get('profile', 'default')
|
|
41
|
+
return Response(MediaAssetSerializer(asset, context={'profile': profile}).data)
|
|
42
|
+
|
|
43
|
+
def partial_update(self, request, pk=None):
|
|
44
|
+
asset = get_object_or_404(MediaAsset, pk=pk)
|
|
45
|
+
changed = False
|
|
46
|
+
for field in ('focal_x', 'focal_y'):
|
|
47
|
+
if field in request.data:
|
|
48
|
+
value = float(request.data[field])
|
|
49
|
+
if not 0 <= value <= 1:
|
|
50
|
+
return Response({field: 'Must be between 0 and 1.'}, status=400)
|
|
51
|
+
setattr(asset, field, value)
|
|
52
|
+
changed = True
|
|
53
|
+
if changed:
|
|
54
|
+
asset.save(update_fields=['focal_x', 'focal_y', 'updated_at'])
|
|
55
|
+
return Response(MediaAssetSerializer(asset).data)
|
|
56
|
+
|
|
57
|
+
@action(detail=True, methods=['post'])
|
|
58
|
+
def regenerate(self, request, pk=None):
|
|
59
|
+
asset = get_object_or_404(MediaAsset, pk=pk)
|
|
60
|
+
profile = request.data.get('profile', 'default')
|
|
61
|
+
enqueue_asset(asset.id, profile=profile, force=True)
|
|
62
|
+
return Response({'id': str(asset.id), 'status': 'queued', 'profile': profile}, status=202)
|
|
63
|
+
|
|
64
|
+
@action(detail=True, methods=['get'])
|
|
65
|
+
def manifest(self, request, pk=None):
|
|
66
|
+
asset = get_object_or_404(MediaAsset, pk=pk)
|
|
67
|
+
profile = request.query_params.get('profile', 'default')
|
|
68
|
+
return Response(build_manifest(asset, profile=profile))
|
|
69
|
+
|
|
70
|
+
@action(detail=True, methods=['get'])
|
|
71
|
+
def panorama(self, request, pk=None):
|
|
72
|
+
asset = get_object_or_404(MediaAsset, pk=pk)
|
|
73
|
+
profile = request.query_params.get('profile', 'panorama.multires')
|
|
74
|
+
manifest = build_manifest(asset, profile=profile)
|
|
75
|
+
adapter = request.query_params.get('adapter', 'generic').lower()
|
|
76
|
+
if adapter == 'pannellum':
|
|
77
|
+
from .adapters.pannellum import to_pannellum
|
|
78
|
+
manifest = to_pannellum(manifest)
|
|
79
|
+
elif adapter == 'marzipano':
|
|
80
|
+
from .adapters.marzipano import to_marzipano
|
|
81
|
+
manifest = to_marzipano(manifest)
|
|
82
|
+
elif adapter == 'threejs':
|
|
83
|
+
from .adapters.threejs import to_threejs
|
|
84
|
+
manifest = to_threejs(manifest)
|
|
85
|
+
elif adapter != 'generic':
|
|
86
|
+
return Response({'adapter': 'Unsupported adapter.'}, status=400)
|
|
87
|
+
return Response(manifest)
|
|
88
|
+
|
|
89
|
+
@action(detail=True, methods=['get'])
|
|
90
|
+
def render(self, request, pk=None):
|
|
91
|
+
asset = get_object_or_404(MediaAsset, pk=pk)
|
|
92
|
+
profile_name = request.query_params.get('profile', 'default')
|
|
93
|
+
profile = get_profile(profile_name)
|
|
94
|
+
fmt = request.query_params.get('format', 'avif').lower()
|
|
95
|
+
width = int(request.query_params.get('width', 640))
|
|
96
|
+
if fmt not in set(profile.get('formats', [])) | {profile.get('fallback_format', 'jpeg')}:
|
|
97
|
+
return Response({'format': 'Unsupported format.'}, status=400)
|
|
98
|
+
width = min(width, asset.width)
|
|
99
|
+
variant = MediaVariant.objects.filter(
|
|
100
|
+
asset=asset,
|
|
101
|
+
profile=profile_name,
|
|
102
|
+
width=width,
|
|
103
|
+
format=fmt,
|
|
104
|
+
processor_version=getattr(settings, 'MEDIA_ENGINE_PIPELINE_VERSION', 1),
|
|
105
|
+
status=MediaVariant.Status.READY,
|
|
106
|
+
).first()
|
|
107
|
+
if variant is None:
|
|
108
|
+
variant = generate_variant(asset, profile_name=profile_name, width=width, fmt=fmt)
|
|
109
|
+
if variant is None:
|
|
110
|
+
return Response({'status': 'processing', 'retry_after': 1}, status=202)
|
|
111
|
+
return HttpResponseRedirect(variant.file.url)
|
|
@@ -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,85 @@
|
|
|
1
|
+
media_engine/__init__.py,sha256=HfAxEc4JmiSOgrNiFiJht-4V2AUW8vplhQrM6QgcvUI,227
|
|
2
|
+
media_engine/admin.py,sha256=X-vE76CV_Ow6ylObT-mm-wD7O-Tl1iw1QhwWtVVIFtU,1588
|
|
3
|
+
media_engine/apps.py,sha256=Dh4H86cECcBR5kLkqZnXcU7xWEPXQnCz4iH0KpmRe6g,319
|
|
4
|
+
media_engine/auth.py,sha256=nd3enjjvKNd788czIqmm7WTXqrPk01OHxb4KduYHGPc,1119
|
|
5
|
+
media_engine/autoregister.py,sha256=WNWDcT2sFUAjuMKavgB84txOQjE8XHWDaLTMJqD3aQc,3731
|
|
6
|
+
media_engine/control_plane.py,sha256=g2xCx7-wgP6KjNQbIe6petfQ4PvAMHcRkAWwIEqDdDI,10094
|
|
7
|
+
media_engine/doctor.py,sha256=o2sn5gW-Pf7wttukRQ3EM3D5Gol-5WZz9tF8_S1AbNk,6122
|
|
8
|
+
media_engine/focal_detectors.py,sha256=qRX6QWC7lYRpsbIjdIHtYAMCymmxDBKJhxazai6E4CA,997
|
|
9
|
+
media_engine/health.py,sha256=DjNxIe8_5gmPjzjhMka7MD_XMyMBAVIEB_edRwm4ttw,2716
|
|
10
|
+
media_engine/image_ops.py,sha256=GgVUVsKMlKd6gPsE9IhfTPC-p9FPVg8pXjuGm9NKPMo,4574
|
|
11
|
+
media_engine/integration.py,sha256=JVZLtet9lmNb0tQ5z2bBVhQJhM4tHkZaaEHiBbgS1YY,1519
|
|
12
|
+
media_engine/locks.py,sha256=kJfPamg26jXTC7O904DnBXMefWwOtEC82rVrAZd0Hig,506
|
|
13
|
+
media_engine/manifest.py,sha256=95LkdM7UEt9Za6oaj5fFO9i2RJYG8G-AJlj-F7mc3v0,3566
|
|
14
|
+
media_engine/metrics.py,sha256=CvYPUt3RTM0AdSk32i9jxp2JDGDDgOr0aPvGll2b_Mc,1648
|
|
15
|
+
media_engine/models.py,sha256=uAUc9lN8pzPiRyAs2TPstPJUzQ--hhJwh7CFpibkinU,6428
|
|
16
|
+
media_engine/node.py,sha256=5lIusUkZ-ALb9c4qGrnP4PsZG8jGM7wmsd09HSMfaxo,887
|
|
17
|
+
media_engine/profiles.py,sha256=4_akGg8J5IQ0Q57kqpbwgHKva-_-DjuJIfXCDrXI2IU,2478
|
|
18
|
+
media_engine/queueing.py,sha256=eqxLlnUIevN6_Pw3temJSsbhFxGYKPQMt9ufA1Wj0OQ,1336
|
|
19
|
+
media_engine/registry.py,sha256=lNqz8AzcpoJglc-yphcfgYiucfjLuU8vSUPqBcSX9I8,1371
|
|
20
|
+
media_engine/runtime_config.py,sha256=4ImMovJGe2vDQnRKDwSC1Ag2dkfjqIyrJmmeN-pNZ6A,1267
|
|
21
|
+
media_engine/serializers.py,sha256=saq7y8tR6gsWsavueX5nbMF_6KeVQLUdchs4Str0Yvw,1308
|
|
22
|
+
media_engine/services.py,sha256=aFtehPz9dyvjW15r2N4T2XqW4KWKQfXSOhOAk1_pD4A,8380
|
|
23
|
+
media_engine/signals.py,sha256=GSUedn5Oe5wT9nVF5vOt-YvBpgtqYkRAFDMjaq5ZoOo,248
|
|
24
|
+
media_engine/storage_paths.py,sha256=wGjWWOH6uPk5kviERlUlBidmyAFJA37g1CIB5Tt0hqM,500
|
|
25
|
+
media_engine/system_views.py,sha256=ik1VGOgprvLA5HMmmkZFff5ONMKKNqaLuVyRoq_acEY,3508
|
|
26
|
+
media_engine/tasks.py,sha256=DY4d2qzTLJRBGg86mDseS8_ty4XSCspj9YnpdMFdgYE,1514
|
|
27
|
+
media_engine/urls.py,sha256=_XVaI5DYdFjJg0UYceRBSndIy5yrms2-GOe0n_gSES0,601
|
|
28
|
+
media_engine/validators.py,sha256=G1IKFh34lHGTftfhJNEXui1orspBVglJEgEHI2K0Iec,1432
|
|
29
|
+
media_engine/views.py,sha256=22T7WMmCCMfbnYwqd9HnbUICEG6FCFI7xQj6uVpNI7w,5110
|
|
30
|
+
media_engine/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
+
media_engine/adapters/flutter/README.md,sha256=j0r7Mecx6OMBM1RXh-7uR6PJjqvpkUoI8BDnG6b_EM4,495
|
|
32
|
+
media_engine/adapters/flutter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
+
media_engine/adapters/flutter/models.dart,sha256=wvnfERfMgtrZAKBzYJPo8ntDrVmn9geC35KVGU-9CnM,2349
|
|
34
|
+
media_engine/adapters/marzipano/__init__.py,sha256=FZiH8ei3WWwnFZXunyFM5uNwznI4HTddTwz1XzWho_E,62
|
|
35
|
+
media_engine/adapters/marzipano/manifest.py,sha256=eXLTAKM_qG0C44AliXGDCIgWvLsBYYhzlOqeVUE6W6Q,1917
|
|
36
|
+
media_engine/adapters/pannellum/__init__.py,sha256=GR2Mhcsj0rf5uLSXTpPTentrUxdrxNA4arafn2a-_qY,62
|
|
37
|
+
media_engine/adapters/pannellum/manifest.py,sha256=RY9DnwvQAPKiEhmRTeXftq5xzL1HvF_qrFsX-aZx00E,753
|
|
38
|
+
media_engine/adapters/threejs/__init__.py,sha256=1DUIYZrcFRHKqBtXTUO2oGhqsh7C-EJI6Uy-3QzzyQk,58
|
|
39
|
+
media_engine/adapters/threejs/manifest.py,sha256=qhYZYKHCl_xyBOJITKqhtgx-CuyQYCV9utWbGQDXN1c,303
|
|
40
|
+
media_engine/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
+
media_engine/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
|
+
media_engine/management/commands/audit_media_engine.py,sha256=91VNMOahisVMp5tTHNzZCzuFSLl8nxGGEAW_jc-Lm5g,2620
|
|
43
|
+
media_engine/management/commands/audit_panorama_media.py,sha256=k-j0ic-RlEO9LVYd5ZX_ThTFNlxI_2llfl5P_DPPktw,998
|
|
44
|
+
media_engine/management/commands/backfill_panorama_media.py,sha256=tw5aOPqZYBEGOqjFzvVTZpSRmsUxYSwDXZ4YODNWEYI,1512
|
|
45
|
+
media_engine/management/commands/backfill_registered_media.py,sha256=jTWb9tWQJL4PfJm80z3C6i8JQ7_9LDlwdR5Q0dc6MfE,840
|
|
46
|
+
media_engine/management/commands/backfill_responsive_images.py,sha256=vfkvDIIQ8-8hEBY6umFKQRTRXDli2BZCfOj_F7q9djA,1157
|
|
47
|
+
media_engine/management/commands/cleanup_image_derivatives.py,sha256=5e3IHQVboI40C8_DwyyhP7xjjhx42NbUbjrSGGWEwZQ,1300
|
|
48
|
+
media_engine/management/commands/media_engine_doctor.py,sha256=TTSan-nOVBhZ6yC7c4lH85fxUwsz5FrnJfKGrxDfniU,510
|
|
49
|
+
media_engine/management/commands/media_engine_node_report.py,sha256=kOOhTRAsyhpY5Rv5ZQo6RIEbmz12R7aSoYhy1M1mzzc,1163
|
|
50
|
+
media_engine/management/commands/rollback_media_engine_config.py,sha256=0T0I7lvALdhHMNk5RP_3UrjSpaRL-fE5Nyy4r6BL9kM,515
|
|
51
|
+
media_engine/management/commands/sync_media_engine_hub.py,sha256=5g91YJsx8feh86yZ5OY7yFdCZbfUzHszxWjsJFYIeAE,787
|
|
52
|
+
media_engine/migrations/0001_initial.py,sha256=lY20ercltw3f_DJmltDIrpnHVUM4X0amvoznNpXj63Y,5226
|
|
53
|
+
media_engine/migrations/0002_panorama_multires.py,sha256=dpKzGuHKJ6CtqJO-xgH56MIrKtnhJ-Jlvk731GmYsKo,3117
|
|
54
|
+
media_engine/migrations/0003_panorama_cube_tiles.py,sha256=lJLQx3k0myJoHcucojtBZWGL6srUcRYQVv7XzMPTZZI,1030
|
|
55
|
+
media_engine/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
|
+
media_engine/processors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
|
+
media_engine/processors/base.py,sha256=tXWzY2m-gYq9qCx9zUrlmJaK6qrmGhgB4bKcilk5cK4,421
|
|
58
|
+
media_engine/processors/registry.py,sha256=Uf-5UvRSm3gyIq6tbU3EKYuwBR2Gk3TUxLpNETc9RN4,546
|
|
59
|
+
media_engine/processors/image/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
60
|
+
media_engine/processors/image/standard.py,sha256=t_uAX8ftaTmR35XBFCi87sG7LACC8Qi5hKeAvOfXL3A,602
|
|
61
|
+
media_engine/processors/panorama/__init__.py,sha256=x8U9y8fzCXPZVrXew_swVXqdF6ITgnNc0RRkvKNJsiY,216
|
|
62
|
+
media_engine/processors/panorama/cube_tiles.py,sha256=ITKi1ZfiQmlI5RfqDAXF3X44VWcz7rcF45baQ4lLg0g,3969
|
|
63
|
+
media_engine/processors/panorama/cubemap.py,sha256=dwWftLKCWOYh5u4wP3guCReAbDotJy5FO8XQsN-TdCc,1423
|
|
64
|
+
media_engine/processors/panorama/detect.py,sha256=nG4mT4syeAUVo441tt95xHLIwrmX-ys7kq8XJEqZ6pQ,898
|
|
65
|
+
media_engine/processors/panorama/equirectangular.py,sha256=CL9HTRb6KwIqFOGO2CLqz7CIWK4x547DTFgnzRCKNdU,7267
|
|
66
|
+
media_engine/processors/panorama/manifest.py,sha256=F7jz9bQnJ8mGJyT90zm7CVFbtnGuBgWgq7eM7EZdBwY,4037
|
|
67
|
+
media_engine/processors/panorama/tiles.py,sha256=IW1WFan8e0Yfmwskmxdwax1A3yEyVF0R45JXn2QrusQ,2956
|
|
68
|
+
media_engine/spec/openapi.yaml,sha256=CD7zoD9ltUFKzITqm9jrNCrBYzlr1EdDB6YiozCDILs,5738
|
|
69
|
+
media_engine/static/media_engine/adaptive-media.js,sha256=ggoeknJBevT3A8T57HRzPunCPyAD8e_P2Gow8_WcY3g,2129
|
|
70
|
+
media_engine/static/media_engine/media-cache-sw.js,sha256=pBMJfCspN0SA1MPU0YNTySYYf3m9BTmANjjhh_CL45U,549
|
|
71
|
+
media_engine/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
72
|
+
media_engine/templatetags/responsive_media.py,sha256=meEdGn5IhGIPvWuktM5RSoQwQxwKUl57CJN-A21bKuE,7341
|
|
73
|
+
media_engine/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
74
|
+
media_engine/tests/test_distributed_node.py,sha256=Ev8TSuZk-te5aGIS9rTq1z8GDzZVCJk0iagFFMro14s,3142
|
|
75
|
+
media_engine/tests/test_image_ops.py,sha256=CG0VRh85I_y7Mv1vBs4Mu61szc0FoOgBLWhc5U-ejCk,494
|
|
76
|
+
media_engine/tests/test_panorama.py,sha256=XHz7hmja3LBX37oALkCl-X1Eg1sofdHcvRoQZ-ljVNY,1332
|
|
77
|
+
media_engine/tests/test_paths.py,sha256=GKdVZQfGEnffxTD4AIWMHZ0qtE-K5AVm1P9nur7QOig,404
|
|
78
|
+
media_engine/tests/test_profiles.py,sha256=oOhdvyRbZGW-TI-TI0BMIoPNSAALhmBnKlzBMQ8SOh4,300
|
|
79
|
+
media_engine/tests/test_responsive_media.py,sha256=zv50HVVIZA6cjLFvrimE9wgKrxwz-YCmhfixNijSVeA,5968
|
|
80
|
+
media_optimization_engine-1.4.0.dist-info/licenses/LICENSE,sha256=1opBmAobedP1rC2gF7n_zFbvJ7tXT6McLaJzLBEmJEE,1073
|
|
81
|
+
media_optimization_engine-1.4.0.dist-info/METADATA,sha256=4RX_SSqTnY7d2ci8Lslubo8ByY7D8VbE3QgqX6ZrC1E,7535
|
|
82
|
+
media_optimization_engine-1.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
83
|
+
media_optimization_engine-1.4.0.dist-info/entry_points.txt,sha256=yqnRhbItLlCUBvkLAH-Waq4Rlz2RXiH5dXzi3VEymjE,65
|
|
84
|
+
media_optimization_engine-1.4.0.dist-info/top_level.txt,sha256=T31aKQHDr9u7GPiEULX4KCNwjssi5x2jsCqZ0EVCscA,13
|
|
85
|
+
media_optimization_engine-1.4.0.dist-info/RECORD,,
|