Publish cache-safe Russian text on site photographs, agent=codex
@@ -3,3 +3,5 @@
|
||||
Исходники и опубликованная сборка находятся в Gitea `german/site-servaki-online`; сайт размещён отдельно на Beget. Перед изменением подтянуть `main`, не включать в Git секреты, резервные копии и локальные зависимости.
|
||||
|
||||
После **каждой генерации для сайта** сохранять в Яндекс Диск все исходные медиа и варианты, промпты, оптимизированные файлы и готовую сборку. Использовать новую датированную папку внутри `D:\YandexDisk\servaki.online\лендинги\servaki.online site\`. Запустить `save_generations_yadisk.py` сначала без `--copy` для плана, затем с `--copy`; указать `--raw-dir` текущего генератора, `--target` папки выпуска и при необходимости `--source` рабочей копии. Проверить итоговый `MANIFEST.json` и число файлов. Не считать запись в локальную папку подтверждением завершённой облачной синхронизации без проверки клиента Яндекс Диска.
|
||||
|
||||
Если на изображении есть читаемые надписи на одежде, вывесках или приборах, использовать осмысленный русский текст кириллицей. Для фото с работой специалиста допустима небольшая метка «СЕРВАКИ»; для иллюстративных объектов — короткая надпись по теме. Проверять каждое слово на полном размере и в опубликованном WebP, исключать случайные латинские буквы и псевдотекст. Не добавлять реальные имена или данные клиентов. Исходный снимок, результат редактирования и точный промпт хранить раздельно.
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
Выпуск с анимацией изображений сохранён в `servaki.online/лендинги/servaki.online site/2026-09-28-motion-release/`; 256 файлов проверены по [манифесту](archive-manifests/2026-09-28-motion-release.json). Эффекты работают только на видимых карточках и выключаются при настройке «уменьшить движение». Все 23 проверенных адреса опубликованного сайта совпали со сборкой.
|
||||
|
||||
Четыре фото с надписями на одежде и вывесках локализованы на русский язык. Версии WebP имеют суффикс `-ru`, чтобы обновление не зависело от кэша браузера; исходные снимки сохранены отдельно. [Архив нового выпуска](archive-manifests/2026-09-28-russian-image-text.json) содержит 289 файлов. На сайте 27 проверенных адресов совпали со сборкой.
|
||||
|
||||
## Сведения о прежней версии
|
||||
|
||||
**Сайт:** https://servaki.online
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- Уникальные title/description/canonical, Open Graph, Article/Service/FAQPage/BreadcrumbList, sitemap, robots.
|
||||
- 12 тематических AI-иллюстраций для карточек главной и 12 тёплых фотореалистичных AI-изображений для внутренних страниц, с WebP-версиями 480/960/1536 px. У каждой статьи своё изображение и подпись о том, что оно создано ИИ без данных и фотографий клиентов.
|
||||
- На страницах услуг — конкретные пояснения о согласовании доступа, ограничении видимости данных и бережной работе в действующем офисе или на объекте. Это описание подхода, а не обещание абсолютной безопасности.
|
||||
- На четырёх тёплых иллюстративных фото уточнены надписи: «ГОСТЕВОЙ ДОМ» и «КАФЕ» на вывесках, «СЕРВАКИ» на одежде специалистов. Написание проверено на полном размере и в WebP. Исходные и отредактированные PNG хранятся отдельно; для новых файлов используется суффикс `-ru`, чтобы браузер не показывал старую закешированную версию.
|
||||
- В тексте каждой из 12 статей — тематическая схема работы оборудования: индикаторы приборов и движение сигнала между ними. Схемы состоят из компактных SVG и CSS, не загружают GIF или видео. При настройке «уменьшить движение» индикаторы и путь остаются статичными.
|
||||
- На 12 иллюстрациях карточек главной, в каталогах и блоках связанных решений поверх WebP добавлены короткие маршруты сигнала и индикаторы работы оборудования. Они не содержат данных клиентов. Анимация включается только при появлении карточки на экране и выключается при настройке «уменьшить движение»; изображение остаётся доступным без JavaScript. Общий вес новых CSS и JS — 2 404 байта. Самая тяжёлая иллюстрация: 37,1 КБ при 480 px, 116,7 КБ при 960 px и 236,2 КБ при 1536 px.
|
||||
- Описания под всеми 12 карточками главной переписаны: задача клиента, способ работы и результат; условия отклика указаны без обещания устранить любой сбой за 15 минут. У трёх каталогов и главной — отдельные поисковые описания.
|
||||
@@ -30,7 +31,7 @@ python -m http.server 5186 --bind 127.0.0.1 --directory public
|
||||
python verify.py
|
||||
```
|
||||
|
||||
`content/pages.json` — тексты; `content/card-copy.json` — тексты карточек; `content/flow-scenes.json` — подписи и оборудование для анимированных схем; `content/image-motion.json` — иллюстративные маршруты сигнала на карточках; `content/image-prompts.json` и `content/photo-prompts.json` — промпты встроенного image_gen; `content/photo-alt.json` и `content/privacy-notes.json` — описания фото и редакционные пояснения; `artwork/` — оригиналы; `assets/` — CSS, JS и WebP; `public/` — готовый сайт; `reference/original.html` — сохранённая исходная главная.
|
||||
`content/pages.json` — тексты; `content/card-copy.json` — тексты карточек; `content/flow-scenes.json` — подписи и оборудование для анимированных схем; `content/image-motion.json` — иллюстративные маршруты сигнала на карточках; `content/image-prompts.json` и `content/photo-prompts.json` — промпты исходной генерации; `content/photo-edit-prompts.json` — точные русские надписи и промпты локализации; `content/photo-alt.json` и `content/privacy-notes.json` — описания фото и редакционные пояснения; `artwork/` — оригиналы и отредактированные фото; `assets/` — CSS, JS и WebP; `public/` — готовый сайт; `reference/original.html` — сохранённая исходная главная.
|
||||
|
||||
## Фактическая основа
|
||||
|
||||
@@ -58,6 +59,8 @@ python verify.py
|
||||
|
||||
Выпуск анимации 28.09.2026: локальная проверка подтвердила 12 анимированных иллюстраций, 16 доступных HTML-страниц, лимиты размеров WebP и неизменные цены. В Chrome проверены каталоги при ширине 1280 и 500 px, а также режим «уменьшить движение». Скрипт `deploy_beget.py update-motion` перед заменой сравнивает все файлы предыдущего выпуска с боевым сайтом и сохраняет изменяемые HTML вне публичного каталога.
|
||||
|
||||
Выпуск русских надписей 28.09.2026: четыре фото получили версионированные файлы `photo-*-ru-{480,960,1536}.webp`; заменены только четыре профильные статьи, исходные WebP сохранены для отката. Перед загрузкой 100 файлов прежнего выпуска сверены с Beget, после неё 27 публичных адресов побайтно совпали со сборкой, TLS действителен. Предыдущие HTML сохранены вне webroot в `/home/i/infogypa/servaki.online/backup-2026-09-28-before-russian-photos/`. Все 289 исходных и готовых файлов с проверенными SHA-256 сохранены в Яндекс Диске `servaki.online/лендинги/servaki.online site/2026-09-28-russian-image-text/`; облачная синхронизация отдельно не подтверждена.
|
||||
|
||||
Браузер: главная и статья на обычном экране и 390×844; горизонтального переполнения нет, responsive images загружаются, оглавление и FAQ работают. Анимированная схема сети визуально проверена на 1280 px и в мобильном окне 390 px: устройства выстраиваются вертикально, путь сигнала сохраняется. Исходная форма на главной остаётся демонстрационной; все новые CTA ведут прямо в Telegram или на телефон, сообщения автоматически не отправлялись.
|
||||
|
||||
## Публикация и откат
|
||||
|
||||
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 102 KiB |
@@ -11,6 +11,7 @@ PAGES=json.loads((ROOT/'content/pages.json').read_text(encoding='utf-8'))
|
||||
BY_ID={p['slug']:p for p in PAGES}
|
||||
PRIVACY=json.loads((ROOT/'content/privacy-notes.json').read_text(encoding='utf-8'))
|
||||
PHOTO_ALT=json.loads((ROOT/'content/photo-alt.json').read_text(encoding='utf-8'))
|
||||
PHOTO_ASSET={item['slug']:item.get('asset','photo-'+item['slug']) for item in json.loads((ROOT/'content/photo-manifest.json').read_text(encoding='utf-8'))}
|
||||
FLOWS=json.loads((ROOT/'content/flow-scenes.json').read_text(encoding='utf-8'))
|
||||
IMAGE_MOTION=json.loads((ROOT/'content/image-motion.json').read_text(encoding='utf-8'))
|
||||
CARD_COPY=json.loads((ROOT/'content/card-copy.json').read_text(encoding='utf-8'))
|
||||
@@ -44,7 +45,7 @@ def image(p,lazy=True,small=False):
|
||||
return f'<img src="/assets/editorial/{slug}-960.webp" srcset="/assets/editorial/{slug}-480.webp 480w, /assets/editorial/{slug}-960.webp 960w, /assets/editorial/{slug}-1536.webp 1536w" sizes="{sizes}" width="1536" height="1024" alt="{esc(p["alt"])}" {"loading=\"lazy\"" if lazy else "fetchpriority=\"high\""} decoding="async">'
|
||||
def photo_image(p):
|
||||
slug=p['slug']
|
||||
return image(p,lazy=False).replace(f'/{slug}-',f'/photo-{slug}-').replace(esc(p['alt']),esc(PHOTO_ALT[slug])).replace('(max-width: 1248px) 100vw, 1200px','(max-width: 600px) calc(100vw - 32px), (max-width: 1000px) calc(100vw - 48px), 520px')
|
||||
return image(p,lazy=False).replace(f'/{slug}-',f'/{PHOTO_ASSET[slug]}-').replace(esc(p['alt']),esc(PHOTO_ALT[slug])).replace('(max-width: 1248px) 100vw, 1200px','(max-width: 600px) calc(100vw - 32px), (max-width: 1000px) calc(100vw - 48px), 520px')
|
||||
def motion_image(p,small=True):
|
||||
scene=IMAGE_MOTION[p['slug']]
|
||||
paths=[]
|
||||
@@ -136,12 +137,12 @@ def build():
|
||||
for width in [480,960,1536]:
|
||||
img=ROOT/'assets'/f'{p["slug"]}-{width}.webp'
|
||||
if img.exists():shutil.copy2(img,dest/img.name)
|
||||
photo=ROOT/'assets'/f'photo-{p["slug"]}-{width}.webp'
|
||||
photo=ROOT/'assets'/f'{PHOTO_ASSET[p["slug"]]}-{width}.webp'
|
||||
if not photo.exists():raise RuntimeError('Missing warm photograph: '+str(photo))
|
||||
shutil.copy2(photo,dest/photo.name)
|
||||
page=article(p)
|
||||
page=page.replace(image(p,lazy=False),photo_image(p),1)
|
||||
page=page.replace(f'/assets/editorial/{p["slug"]}-1536.webp',f'/assets/editorial/photo-{p["slug"]}-1536.webp')
|
||||
page=page.replace(f'/assets/editorial/{p["slug"]}-1536.webp',f'/assets/editorial/{PHOTO_ASSET[p["slug"]]}-1536.webp')
|
||||
page=page.replace(' · Тематическая AI-иллюстрация',' · Изображение создано ИИ, без данных и фотографий клиентов',1)
|
||||
page=page.replace('<article><header class="intro">','<article><div class="article-top"><header class="intro">',1)
|
||||
page=page.replace('</figure><div class="strengths">','</figure></div><div class="strengths">',1)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"mode": "built-in image_gen, text-localization edit",
|
||||
"rule": "If any text is visible in a site image, use meaningful, correctly spelled Russian Cyrillic. Verify every word at full resolution; avoid invented client details, Latin letters and gibberish.",
|
||||
"edits": {
|
||||
"video-surveillance": {
|
||||
"source": "artwork/photos-selected/video-surveillance.png",
|
||||
"result": "artwork/photos-localized/video-surveillance.png",
|
||||
"exact_text": ["ГОСТЕВОЙ ДОМ", "СЕРВАКИ"],
|
||||
"placement": "Wooden guesthouse sign and technician's navy polo",
|
||||
"prompt": "Preserve the people, camera, stone courtyard, lighting and composition. Add ГОСТЕВОЙ ДОМ in readable Cyrillic to the blank wooden sign, and a small СЕРВАКИ embroidered label to the technician's polo. No other text or changes."
|
||||
},
|
||||
"horeca": {
|
||||
"source": "artwork/photos-selected/horeca.png",
|
||||
"result": "artwork/photos-localized/horeca.png",
|
||||
"exact_text": ["КАФЕ", "СЕРВАКИ"],
|
||||
"placement": "Chalkboard and technician's dark jacket",
|
||||
"prompt": "Preserve the people, access point, tablet, wood interior and composition. Add КАФЕ in readable Cyrillic on the blank chalkboard and a small СЕРВАКИ embroidered label on the technician's jacket. No other text or changes."
|
||||
},
|
||||
"construction": {
|
||||
"source": "artwork/photos-selected/construction.png",
|
||||
"result": "artwork/photos-localized/construction.png",
|
||||
"exact_text": ["СЕРВАКИ"],
|
||||
"placement": "Technical specialist's black jacket",
|
||||
"prompt": "Preserve both people, camera, plans, safety gear, building, lighting and composition. Add one small correctly spelled Cyrillic СЕРВАКИ label on the technical specialist's black jacket. No other text or changes."
|
||||
},
|
||||
"agriculture": {
|
||||
"source": "artwork/photos-selected/agriculture.png",
|
||||
"result": "artwork/photos-localized/agriculture.png",
|
||||
"exact_text": ["СЕРВАКИ"],
|
||||
"placement": "Technical specialist's dark jacket",
|
||||
"prompt": "Preserve the farmers, specialist, sensor, laptop, farm landscape, lighting and composition. Add one small correctly spelled Cyrillic СЕРВАКИ label on the technical specialist's dark jacket. No other text or changes."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,8 @@
|
||||
},
|
||||
{
|
||||
"slug": "video-surveillance",
|
||||
"path": "artwork/photos-selected/video-surveillance.png"
|
||||
"path": "artwork/photos-localized/video-surveillance.png",
|
||||
"asset": "photo-video-surveillance-ru"
|
||||
},
|
||||
{
|
||||
"slug": "servers-hosting",
|
||||
@@ -37,14 +38,17 @@
|
||||
},
|
||||
{
|
||||
"slug": "horeca",
|
||||
"path": "artwork/photos-selected/horeca.png"
|
||||
"path": "artwork/photos-localized/horeca.png",
|
||||
"asset": "photo-horeca-ru"
|
||||
},
|
||||
{
|
||||
"slug": "agriculture",
|
||||
"path": "artwork/photos-selected/agriculture.png"
|
||||
"path": "artwork/photos-localized/agriculture.png",
|
||||
"asset": "photo-agriculture-ru"
|
||||
},
|
||||
{
|
||||
"slug": "construction",
|
||||
"path": "artwork/photos-selected/construction.png"
|
||||
"path": "artwork/photos-localized/construction.png",
|
||||
"asset": "photo-construction-ru"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -252,6 +252,61 @@ def update_motion():
|
||||
sftp.close(); ssh.close()
|
||||
|
||||
|
||||
def update_localized_photos():
|
||||
"""Publish four Russian-lettered photos under new cache-safe asset names."""
|
||||
previous = ROOT.parent / 'site-servaki-online-git' / 'public'
|
||||
if not previous.is_dir():
|
||||
raise RuntimeError('Previous Gitea release is required for the baseline check')
|
||||
old = {p.relative_to(previous).as_posix(): digest(p.read_bytes())
|
||||
for p in previous.rglob('*') if p.is_file()}
|
||||
new = manifest()
|
||||
slugs = ('video-surveillance', 'horeca', 'agriculture', 'construction')
|
||||
pages = {'services/video-surveillance/index.html',
|
||||
*(f'industries/{slug}/index.html' for slug in slugs if slug != 'video-surveillance')}
|
||||
assets = {f'assets/editorial/photo-{slug}-ru-{width}.webp'
|
||||
for slug in slugs for width in (480, 960, 1536)}
|
||||
changed = {rel for rel in new if new[rel] != old.get(rel)}
|
||||
if set(old) - set(new) or changed != pages | assets:
|
||||
raise RuntimeError('Unexpected Russian-photo release scope: ' + repr(sorted(changed ^ (pages | assets))))
|
||||
local_backup = ROOT / 'release-backups' / '2026-09-28-before-russian-photos'
|
||||
remote_backup = SITE + '/backup-2026-09-28-before-russian-photos'
|
||||
ssh, sftp = connect()
|
||||
try:
|
||||
for rel, expected in old.items():
|
||||
target = WEBROOT + '/' + rel
|
||||
if not exists(sftp, target) or digest(remote_bytes(sftp, target)) != expected:
|
||||
raise RuntimeError('Published baseline changed; stopped before writing: ' + rel)
|
||||
for rel in assets:
|
||||
if exists(sftp, WEBROOT + '/' + rel):
|
||||
raise RuntimeError('New localized asset already exists: ' + rel)
|
||||
mkdirs(sftp, remote_backup)
|
||||
for rel in sorted(pages):
|
||||
data = remote_bytes(sftp, WEBROOT + '/' + rel)
|
||||
destination = local_backup / rel
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if destination.exists() and destination.read_bytes() != data:
|
||||
raise RuntimeError('Local backup conflict: ' + rel)
|
||||
destination.write_bytes(data)
|
||||
remote_copy = remote_backup + '/' + rel
|
||||
if exists(sftp, remote_copy):
|
||||
if digest(remote_bytes(sftp, remote_copy)) != old[rel]:
|
||||
raise RuntimeError('Remote backup conflict: ' + rel)
|
||||
else:
|
||||
put_atomic(sftp, destination, remote_copy)
|
||||
for rel in sorted(assets):
|
||||
put_atomic(sftp, ROOT / 'public' / rel, WEBROOT + '/' + rel)
|
||||
for rel in sorted(pages):
|
||||
put_atomic(sftp, ROOT / 'public' / rel, WEBROOT + '/' + rel,
|
||||
replace=True, backup_dir=remote_backup)
|
||||
for rel in changed:
|
||||
if digest(remote_bytes(sftp, WEBROOT + '/' + rel)) != new[rel]:
|
||||
raise RuntimeError('Published localized photo failed checksum: ' + rel)
|
||||
print('Published Russian-lettered photos; changed files:', len(changed),
|
||||
'verified previous files:', len(old), 'backup:', remote_backup)
|
||||
finally:
|
||||
sftp.close(); ssh.close()
|
||||
|
||||
|
||||
def reprice():
|
||||
"""Replace only the published homepage after verifying the six price tokens."""
|
||||
before = json.loads((BACKUP / 'deployed.json').read_text(encoding='utf-8'))
|
||||
@@ -304,4 +359,5 @@ def reprice():
|
||||
|
||||
if __name__ == '__main__':
|
||||
{'audit': audit, 'deploy': deploy, 'verify': verify,
|
||||
'reprice': reprice, 'update-motion': update_motion}[sys.argv[1]]()
|
||||
'reprice': reprice, 'update-motion': update_motion,
|
||||
'update-localized-photos': update_localized_photos}[sys.argv[1]]()
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"""Preserve generated editorial photographs and export responsive WebP versions."""
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw, ImageOps
|
||||
import json, shutil
|
||||
import json
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
manifest=json.loads((ROOT/'content/photo-manifest.json').read_text(encoding='utf-8'))
|
||||
originals=ROOT/'artwork/photos-selected'; originals.mkdir(parents=True,exist_ok=True)
|
||||
tiles=[]
|
||||
for item in manifest:
|
||||
slug=item['slug']; source=ROOT/item['path']
|
||||
dest=originals/(slug+'.png')
|
||||
if source.resolve()!=dest.resolve():shutil.copy2(source,dest)
|
||||
slug=item['slug']; source=ROOT/item['path']; asset=item.get('asset','photo-'+slug)
|
||||
picture=Image.open(source).convert('RGB')
|
||||
picture=ImageOps.fit(picture,(1536,1024),method=Image.Resampling.LANCZOS,centering=(.5,.5))
|
||||
for width in (480,960,1536):
|
||||
picture.resize((width,round(width*2/3)),Image.Resampling.LANCZOS).save(ROOT/'assets'/f'photo-{slug}-{width}.webp',quality=83,method=6)
|
||||
picture.resize((width,round(width*2/3)),Image.Resampling.LANCZOS).save(ROOT/'assets'/f'{asset}-{width}.webp',quality=83,method=6)
|
||||
tile=Image.new('RGB',(384,280),'#05070f')
|
||||
tile.paste(picture.resize((384,256)),(0,0))
|
||||
ImageDraw.Draw(tile).text((12,263),slug,fill='#67e8f9')
|
||||
|
||||
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 102 KiB |
@@ -60,6 +60,10 @@ def main():
|
||||
scenes=json.loads((ROOT/'content/flow-scenes.json').read_text(encoding='utf-8'))
|
||||
image_motion=json.loads((ROOT/'content/image-motion.json').read_text(encoding='utf-8'))
|
||||
pages=json.loads((ROOT/'content/pages.json').read_text(encoding='utf-8'))
|
||||
photo_manifest=json.loads((ROOT/'content/photo-manifest.json').read_text(encoding='utf-8'))
|
||||
photo_edits=json.loads((ROOT/'content/photo-edit-prompts.json').read_text(encoding='utf-8'))['edits']
|
||||
assert len(photo_manifest)==len(pages)==12,'photo manifest coverage'
|
||||
assert set(photo_edits)=={'video-surveillance','horeca','agriculture','construction'},'localized photo coverage'
|
||||
card_copy=json.loads((ROOT/'content/card-copy.json').read_text(encoding='utf-8'))
|
||||
assert set(scenes)=={p['slug'] for p in pages},'missing or extra equipment scene'
|
||||
assert set(image_motion)==set(scenes),'missing or extra animated illustration'
|
||||
@@ -108,7 +112,8 @@ def main():
|
||||
else:raise AssertionError('Negative control did not detect broken image')
|
||||
locations=[e.text for e in ET.parse(PUBLIC/'sitemap.xml').iter() if e.tag.endswith('}loc')]
|
||||
assert len(locations)==16 and len(set(locations))==16,'sitemap pages'
|
||||
small_photos=list((PUBLIC/'assets/editorial').glob('photo-*-480.webp'))
|
||||
small_photos=[PUBLIC/'assets/editorial'/f'{item.get("asset","photo-"+item["slug"])}-480.webp' for item in photo_manifest]
|
||||
assert all(p.is_file() for p in small_photos),'missing current article photograph'
|
||||
assert len(small_photos)==len(pages) and max(p.stat().st_size for p in small_photos)<50000,'oversized mobile photograph'
|
||||
illustration_budgets={480:50000,960:130000,1536:250000}
|
||||
illustration_max_bytes={}
|
||||
|
||||
@@ -42,6 +42,10 @@ def main():
|
||||
PUBLIC / 'assets/editorial/motion.css',
|
||||
PUBLIC / 'assets/editorial/motion.js',
|
||||
PUBLIC / 'assets/editorial/photo-networks-vpn-960.webp',
|
||||
PUBLIC / 'assets/editorial/photo-video-surveillance-ru-960.webp',
|
||||
PUBLIC / 'assets/editorial/photo-horeca-ru-960.webp',
|
||||
PUBLIC / 'assets/editorial/photo-agriculture-ru-960.webp',
|
||||
PUBLIC / 'assets/editorial/photo-construction-ru-960.webp',
|
||||
PUBLIC / 'assets/editorial/networks-vpn-480.webp']
|
||||
mismatches = []
|
||||
for file in selected:
|
||||
|
||||