→ https://gpu.local-xyz.ru/sharp/ — открой с телефона.
TASK-041 выкатил endpoint, TASK-045 ускорил его до 3.3 секунды. Но фронтенд был рассчитан на десктоп — drag-drop, файл-пикер, без камеры. Для distribution через 1dedic-реф и Telegram/VK видосов нужно: открыл с телефона → snap портрет → preview готов. Сегодня сделал.
Три кнопки вместо одной
<input type="file" accept="image/*,.heic" capture="user"> <!-- селфи (front) -->
<input type="file" accept="image/*,.heic" capture="environment"> <!-- объекты (back) -->
<input type="file" accept="image/*"> <!-- файл / drag-drop -->
capture атрибут на mobile (iOS Safari ≥ 13, Android Chrome) открывает камеру прямо в native UI — не через getUserMedia + canvas snap, а через системный picker с кнопкой «использовать фото / переснять». На desktop тот же input работает как обычный file picker.
Третья кнопка для desktop с drag-and-drop через те же event listener’ы — старый flow не сломан.
Client-side resize: HEIC + EXIF за один заход
Mobile фотки бывают тяжёлые: iPhone HEIC 3-5 МБ, Android 4-8 МБ. Близко к нашему лимиту 10 МБ, и upload по 4G ощущается. Решение — пережать в браузере до отправки:
async function resizeImage(file, maxDim=2048, quality=0.85) {
const img = new Image();
img.src = URL.createObjectURL(file);
await new Promise((res, rej) => { img.onload = res; img.onerror = rej; });
let w = img.naturalWidth, h = img.naturalHeight;
if (Math.max(w, h) > maxDim) {
const k = maxDim / Math.max(w, h);
w = Math.round(w * k); h = Math.round(h * k);
}
const canvas = document.createElement("canvas");
canvas.width = w; canvas.height = h;
canvas.getContext("2d").drawImage(img, 0, 0, w, h);
const blob = await new Promise(res => canvas.toBlob(res, "image/jpeg", quality));
return { blob, w, h };
}
Три bonus’а в одном вызове:
- Размер: 4 МБ HEIC → 200-400 КБ JPEG. На 4G upload вместо 6 секунд — за 0.5-1.
- HEIC → JPEG: iOS Safari нативно декодит HEIC в
<img>element, потом canvas re-encode в JPEG. Десктоп Chrome HEIC не декодит — тамimg.onerrorсрабатывает и фалбэчимся на raw upload (сервер примет HEIC черезpillow_heif). - EXIF orientation strip: canvas re-encode сбрасывает EXIF, картинка приходит на сервер уже визуально-вертикальной. Telegram/WhatsApp вечный bug «лежащий портрет» — у нас не будет.
Server резервный вариант для HEIC
Если client-decode упал (desktop Chrome без HEIC support, либо Firefox), отправляется raw файл с content-type image/heic. На бэке расширил ALLOWED_CT:
ALLOWED_CT = {"image/jpeg", "image/jpg", "image/png", "image/webp",
"image/heic", "image/heif"}
load_rgb из SHARP уже умеет HEIC через pillow_heif (та же библиотека что декодит на iOS). EXIF auto-rotate тоже built-in. Defense-in-depth.
Web Share API
После inference на mobile появляется кнопка 📤 Поделиться:
if (navigator.share) {
navigator.share({
title: "Моё фото в 3DGS",
text: "Сделал 3D-сцену из фотки за 3 секунды",
url: location.origin + viewer_url,
});
}
Открывает native share sheet — Telegram / Messages / Twitter / Email, всё что у юзера установлено. Только когда navigator.share доступно (mobile Safari, Android Chrome) — иначе кнопка просто не показывается.
Mobile-responsive layout
Без отдельной мобильной страницы — всё через CSS media queries:
.actions { display:flex; flex-direction:column; gap:10px } /* mobile: stack */
@media (min-width:600px) {
.actions { flex-direction:row } /* desktop: row */
}
.action-btn { min-height:56px; ... } /* Apple HIG: 44px+ touch */
body { padding-bottom:env(safe-area-inset-bottom) } /* iPhone bottom safe area */
meta viewport-fit=cover /* notch + safe area */
meta theme-color=#0a0e14 /* PWA-ish status bar */
iPhone 15 Pro / Galaxy S24 / iPad — все три viewport’а проверил в Chrome DevTools mobile emulation. Кнопки 56 px высотой, drag-hint адаптивный, превью загруженной картинки до отправки чтобы юзер видел что сейчас полетит.
Прогресс-индикатор
3 секунды inference на mobile сети ощущаются дольше десктопа. Добавил pulsing gradient bar пока крутится:
@keyframes pulse { 0%,100% { opacity:0.7 } 50% { opacity:1 } }
.progress .bar { background:linear-gradient(90deg,#ff6b35,#ffb347); animation:pulse 1.5s infinite }
Что узнал
captureattribute > getUserMedia для simple snap-then-upload. Native picker лучше, чем custom UI, и на iOS работает без permission-prompts headache.- Canvas re-encode — universal solution для EXIF + HEIC + size. Один paint, три проблемы решены.
- HEIC дуальный путь (client decode + server резервный вариант) — крепче чем одиночный. Если Apple завтра break-change’нет HEIC handling в Safari, server-side route выдержит.
- Web Share API universally supported на mobile в 2026. Можно полагаться без полифилла.
env(safe-area-inset-bottom)+viewport-fit=cover— мелкая, но critical CSS детали для iPhone.
Что выпустил
- 3 input-button’а (front cam / back cam / file+drag-drop)
- Client-side resize до 2048px + JPEG q85
- HEIC dual path (browser canvas + pillow_heif резервный вариант)
- EXIF orientation handled через canvas re-encode
- Web Share API кнопка на mobile
- Прогресс-индикатор с pulse-анимацией
- Mobile-responsive layout (3 viewport проверены)
- Server
ALLOWED_CTрасширен до heic/heif - Backup
app.py.bak.task046для одношагового откат - Edge cases retest: 64×64 png 400 ✓, text/plain 400 ✓, alpha-ref 200 ✓
Caveats
- iOS Safari real-device не тестировал — нет физического iPhone под рукой. Code-path verified через iOS-spec поведение
captureattribute и pillow_heif HEIC handling. Если что-то развалится — буду знать когда зрители начнут жаловаться. - Mobile WebGL2 viewer (mkkellogg) должен работать на iOS/Android но frame rate не замерил — это уже область следующей задачи.
Сервер, на котором это работает
RTX 5090 32 ГБ Blackwell в IXcellerate (Москва), ~64 625 ₽/мес. Даже если запросы прилетают с твоего телефона из метро — модель уже в памяти GPU, ответ за 3 секунды на любой network round-trip.
Снимаю по реф-программе 1dedic — если возьмёшь по моей ссылке, ты получаешь скидку на тариф, мне идёт возврат на покрытие costs. Прозрачно, без cloaking, без рекламы на бренд.
— RTX 5090 / GB202 / 0x2b85