Recording, playback, and screen capture. Recording indicator mandatory; codecs via engines/kodec; widgets KoderVideoPlayer / KoderVideoRecorder / KoderScreenCapture.
Image
Contrato cross-surface para captura, seleção e preview de imagens em apps Koder. Toggles de privacidade (`media.image.camera`, `media.image.gallery`) em Settings, defaults seguros (tudo OFF até o primeiro use ativar via prompt), formatos canônicos (JPEG/PNG/WebP/AVIF in; JPEG/PNG out), e widgets compartilhados `KoderImagePicker` + `KoderImagePreview` + `KoderImageCropper` em `engines/sdk/koder_kit`. EXIF stripping obrigatório em upload por default. `<APP>-IMAGE-*` error map. Cross-link com `specs/media/video.kmd` (camera permission compartilhada) e `specs/multi-tenancy/contract.kmd` (storage path prefix).
Full specification
Media — Image Spec — v0.1
Normative cross-surface spec para image I/O em apps Koder. Implementação obrigatória via widgets
Koder*Image*emengines/sdk/koder_kit(Flutter) ekoder_web_kit(JS) — nunca rolar<input type="file">ouImagePickerupstream local.
Scope
Aplica-se a todo app Koder que exiba, capture, ou faça upload de
imagens: avatar do usuário, attachments em chat, galerias de produto,
captura de documento via câmera (cross-cuts document.kmd quando o
intuito é OCR), upload de logo em settings de workspace, etc.
Surfaces cobertas: Flutter mobile (Android + iOS), Flutter desktop (Linux + macOS + Windows), Flutter web ou templ+HTMX, TV (raro — só quando há media remota; câmera local em TV é fora de escopo). CLI/TUI recebem path arg como hoje.
1 — MUST: expose "Imagem" toggle group in Settings
Apps Koder com mic OU câmera capability devem ter um agrupamento
"Mídia" (Media em en-US) na tela de Settings; a sub-seção "Imagem"
deve conter, na ordem:
- Câmera (
media.image.camera) — toggle MUST gate à camera - Galeria (
media.image.gallery) — toggle MUST gate ao file picker - EXIF strip em upload (
media.image.strip_exif) — checkbox - Qualidade de compressão JPEG (
media.image.jpeg_quality) — slider 50-100, default 85
A implementação é via drop-in KoderMediaSettingsTile do koder_kit
(host-side da §1 do voice/wake-word.kmd pattern). Nunca desenhar
a tile localmente.
2 — MUST: defaults seguros em fresh install
| Chave | Default | Notas |
|---|---|---|
media.image.camera | OFF | Privacy-by-default; primeiro uso dispara permission prompt do SO e atualiza este toggle pra ON |
media.image.gallery | OFF | Idem para Photos/Files permission |
media.image.strip_exif | ON | GPS + camera serial + timestamp removidos antes do upload; modo OFF exige opt-in explícito do usuário |
media.image.jpeg_quality | 85 | Range 50-100; abaixo de 50 a qualidade fica visivelmente ruim, acima de 95 o ganho é marginal |
Quando media.image.camera for toggled OFF em runtime, o app deve
parar imediatamente qualquer capture ativo + liberar handle da câmera.
3 — MUST: privacidade de captura
Image data nunca sai do dispositivo sem ação explícita do usuário (tap em "Enviar", drop em conversation, save em form). Especificamente:
- NUNCA auto-upload de preview (a UI mostra preview local; user confirma antes do POST)
- NUNCA enviar imagens a serviços terceiros (analytics, error
reporter, etc.); imagens só vão pro endpoint Koder declarado em
KoderApp.config().mediaEndpoint - NUNCA persistir imagem em diretório acessível por outro app
sem ser explicitamente
Pictures/Koder/<app>/(consentido) - EXIF strip é default ON (§2); se OFF, o app deve mostrar warning no flow de upload ("Esta imagem inclui localização GPS e metadados de câmera. Continuar?")
4 — MUST: widget surface no koder_kit
Widgets canônicos a usar (zero implementação local):
| Widget | Função |
|---|---|
KoderImagePicker | Sheet com 2 ações: "Tirar foto" (gates media.image.camera) e "Escolher da galeria" (gates media.image.gallery); retorna KoderImageRef |
KoderImagePreview | Render de KoderImageRef com fallback (placeholder + retry) e progress overlay durante upload |
KoderImageCropper | Crop UI com aspect ratio fixo (1:1, 4:5, 16:9, free); retorna nova KoderImageRef cropped |
KoderAvatarPicker | Helper de alto nível: pick → crop 1:1 → upload pra media/avatar/ (path-prefix conforme multi-tenancy/contract.kmd) |
KoderImageThumbnail | Render de thumb 128px (default) ou custom size, com lazy-load + LRU cache de 50 MB on-device |
KoderImageRef é um value type: {uri: String, mime: String, sizeBytes: int, width: int, height: int, koderUserId: String?, workspaceId: String?}. Apps nunca lidam com bytes brutos —
o widget cuida.
5 — MUST: format support
Input (decode): JPEG, PNG, WebP, AVIF, HEIC (iOS), GIF (still frame só).
Output (encode): JPEG (default, qualidade da §2) ou PNG (lossless
quando media.image.format = png). Apps nunca uploadam HEIC raw —
o widget converte para JPEG/PNG antes do POST.
Resize policy (default): dimensão maior > 2048 px → resize para
2048 mantendo aspect ratio. Configurável via
KoderImagePicker(maxDimension: 4096).
Upload size cap: 25 MB pós-compress. Exceder → erro
<APP>-IMAGE-SIZE-001 com sugestão de qualidade menor.
6 — MUST: error surface
Erros de imagem que cheguem ao usuário devem seguir
specs/errors/user-facing-messages.kmd: texto humanizado em
pt-BR/en-US, botão "Ver detalhes", ID <APP>-IMAGE-<CODE>-<SEQ>.
| Cenário | ID | Texto pt-BR |
|---|---|---|
| Permissão de câmera negada | <APP>-IMAGE-CAM-001 | "Permita o acesso à câmera para tirar fotos." |
| Permissão de galeria negada | <APP>-IMAGE-GAL-001 | "Permita o acesso às fotos para escolher uma imagem." |
| Formato não suportado | <APP>-IMAGE-FMT-001 | "Este formato de imagem não é suportado. Use JPEG ou PNG." |
| Tamanho excede 25 MB pós-compress | <APP>-IMAGE-SIZE-001 | "Imagem muito grande. Reduza a qualidade ou escolha outra." |
| Upload falhou (rede / endpoint indisponível) | <APP>-IMAGE-NET-001 | "Falha ao enviar a imagem. Tente novamente." |
| Decode falhou (arquivo corrompido) | <APP>-IMAGE-DEC-001 | "Não foi possível abrir esta imagem (arquivo corrompido)." |
7 — Observability
- Counters:
media.image.picked,media.image.cropped,media.image.uploaded,media.image.upload_error - Latency histograms:
media.image.compress_ms,media.image.upload_ms - Não emitir métricas que vazem conteúdo (file path, EXIF, hash da imagem) — apenas counters + latência
8 — Adoption checklist (per app)
Quando um app Koder ganha image capability, os pull requests devem incluir:
- Importa
KoderMediaSettingsTile(sub-seção Imagem visível) - Usa
KoderImagePicker(nuncaImagePickerupstream /<input type="file">raw) - Defaults da §2 respeitados em fresh install
- EXIF strip ON por default + warning quando OFF (§3)
- Formats da §5 respeitados (HEIC → JPEG antes do POST)
- Error map da §6 implementado
- Upload path respeita
multi-tenancy/contract.kmd(/u/<koder_user_id>/.../)
Non-normative — referências
- Sibling specs:
specs/media/video.kmd(camera shared),specs/media/document.kmd(camera→OCR cross-cut),specs/media/audio.kmd(mic equivalent pattern) - Voice equivalent:
specs/voice/wake-word.kmd(precedente para o pattern de Settings tile + privacy contract) - Implementation surface:
engines/sdk/koder_kit/lib/src/media/— ainda não existe (criar junto com KSTACK ticket de adoção) - Server-side storage:
specs/multi-tenancy/contract.kmd(path-prefix obrigatório) +specs/koder-app/behaviors.kmd§3 (auth scope)
Other media capabilities
Memos, podcasts, attachments, ringtones — generic audio, separate from voice/wake-word. Widgets KoderAudioPlayer / KoderAudioRecorder.
Pick, preview, and OCR for PDF/DOCX/MD/TXT. Local-first OCR (tesseract); fallback to services/ai/ocr. Widgets KoderDocumentPicker / KoderOcrButton.