# API — Análise de imagem (moderação NSFWJS)

Documentação complementar ao OpenAPI (`POST /api/analise/imagem`). O classificador é o **NSFWJS** no servidor (variável `NSFW_CLASSIFY_URL`, por defeito `http://127.0.0.1:3000/classify`).

## Finalidade

Permitir que a **app** valide uma imagem **antes** de enviar URLs definitivas (avatar, vitrine, revisão). O backend:

1. Recebe **uma** fonte: ficheiro multipart `image` **ou** campo de formulário `url`.
2. Se for `url`, descarrega para um ficheiro temporário (com limite de tamanho e bloqueio de redes privadas — ver abaixo).
3. Se o conteúdo for **WebP**, converte para **JPEG** temporário (GD ou Imagick) para compatibilidade com o classificador.
4. Envia o binário ao NSFWJS (campo `image`).
5. Aplica as **regras de aprovação** documentadas aqui.
6. **Apaga** sempre o ficheiro temporário.
7. Responde em caso de sucesso HTTP **200** com `aprovado`, `predominante`, `analise` (cada classe com `probabilidade` 0–1 e `percentual`), e `medidor_risco_percentual` (soma Porn+Hentai+Sexy em %).

Erros devolvem `{ "erro": "<código>", "mensagem": "…" }` conforme OpenAPI.

**URLs com query (`?ims=…`) ou CDN com `Content-Type: application/octet-stream`:** o servidor infere o formato pelos **primeiros octetos** do ficheiro (JPEG `FF D8 FF`, PNG, WebP, …), opcionalmente com `exif_imagetype`, renomeia o temporário para a **extensão correta** e envia ao NSFWJS com MIME adequado. Não depende da extensão no URL.

**WebP:** antes de enviar ao NSFWJS, ficheiros **WebP** são convertidos para **JPEG** no servidor (GD `imagecreatefromwebp` ou **Imagick**). Se nenhum estiver disponível, resposta `422` com `conversao_webp_indisponivel`.

## Autenticação

**Não é necessário JWT.** A rota é pública (como cidades/ofertas em leitura); ideal para o cliente chamar antes de escolher/submeter uma imagem.

**Nota:** Em produção podes proteger por **rate limiting** ou firewall na frente do PHP; isso fica fora deste repositório.

## Pedido (`multipart/form-data`)

| Campo | Obrigatório | Descrição |
|--------|-------------|-----------|
| `image` | Condicional | Ficheiro imagem (JPEG, PNG, WebP, etc.). **Não** enviar em conjunto com `url`. |
| `url` | Condicional | URL `http` ou `https` de uma imagem pública. Sem utilizador/senha na URL. **Não** enviar em conjunto com `image`. |

## Resposta de sucesso (`200`)

Corpo JSON (campos estáveis):

| Campo | Descrição |
|--------|-----------|
| `aprovado` | `true` / `false` segundo as **regras de negócio** do servidor (não é só «classe no topo»). |
| `predominante` | Objeto `{ "classe", "probabilidade", "percentual" }` — maior probabilidade devolvida pelo modelo (0–1 e ×100). |
| `analise` | Lista com **todas** as classes na mesma forma, ordenadas como o NSFWJS. |
| `medidor_risco_percentual` | Soma **Porn + Hentai + Sexy** em percentagem (referência; **não** é o único critério de `aprovado`). |

Exemplo:

```json
{
  "aprovado": true,
  "predominante": { "classe": "Neutral", "probabilidade": 0.9448, "percentual": 94.48 },
  "analise": [
    { "classe": "Neutral", "probabilidade": 0.9448, "percentual": 94.48 },
    { "classe": "Drawing", "probabilidade": 0.031, "percentual": 3.1 }
  ],
  "medidor_risco_percentual": 5.2
}
```

Assim a app mostra **percentuais e probabilidades** mesmo quando `aprovado` é `true`, para transparência e debug.

## Regras de `aprovado` (servidor)

Estas regras foram afinadas para reduzir **falsos positivos** em fotos normais (ex.: crianças a brincar, supermercado), onde o modelo às vezes inflaciona `Porn` sem que essa seja a classe mais provável. **Não** se usa a soma ingénua `Porn + Hentai + Sexy` como critério único.

Ordem de avaliação (lista já ordenada pelo modelo, maior probabilidade em primeiro lugar):

1. **Topo `Porn` ou `Hentai`**  
   - `aprovado = false` se a probabilidade do topo for **≥ 0,55**.  
   - Caso contrário `true` (confiança baixa no topo explicito).

2. **Topo `Sexy`**  
   - `aprovado = false` se probabilidade **≥ 0,58** (antes 0,65 — mais restritivo em conteúdo claramente sensual).

3. **Topo `Neutral` ou `Drawing`**  
   - `aprovado = false` se o **2.º resultado** for `Sexy` com probabilidade **≥ 0,24** **e** a diferença para o topo for **&lt; 0,22** (empate — típico em **colagens** ou imagens híbridas).  
   - `aprovado = false` se **simultaneamente**: `Sexy ≥ 0,30` **e** `(Porn + Hentai) ≥ 0,06`.  
   - `aprovado = false` se **`Sexy ≥ 0,33`** **e** a probabilidade do topo for **≤ 0,58** (vitória fraca de Neutral com sensualidade relevante).  
   - `aprovado = false` se **Sexy ≥ 0,65** e **Sexy + 0,04 ≥ probabilidade do topo**.  
   - Caso contrário `true` (ex.: crianças a brincar com Neutral confortável no topo e Sexy moderado).

**Nota:** O classificador vê **uma** imagem inteira; **montagens lado a lado** podem enganar menos com as regras acima, mas não substituem revisão humana para casos limite.

4. **Qualquer outro caso** → `false` (conservador).

Constantes estão no código: `ServicoModeracaoImagemNsfw::decidirAprovacao()`.

## Descarga por URL (segurança)

- Esquemas permitidos: **http**, **https** apenas.
- URLs com utilizador ou palavra-passe (`http://user@…`) são recusadas.
- **localhost** e IPs/resoluções em redes privadas ou reservadas são recusadas (`url_proibida`).
- Tamanho máximo configurável: `NSFW_DOWNLOAD_MAX_BYTES` (predefinição 10 MiB).

## Variáveis de ambiente

| Variável | Descrição |
|----------|-----------|
| `NSFW_CLASSIFY_URL` | Endpoint POST multipart do classificador. |
| `NSFW_DOWNLOAD_MAX_BYTES` | Máximo ao descarregar por `url`. |
| `APP_DEBUG` / `NSFW_TEST_PAGE_ENABLED` | Página HTML de teste: `GET /teste-moderacao-imagem` (chama esta API no browser). |

## Teste manual

1. Abrir `/teste-moderacao-imagem`, escolher ficheiro **ou** URL, clicar «Chamar API».
2. Ou usar Swagger em `/documentacao/` na operação **Análise de imagem** (sem Authorize).

## Limitações

- O modelo é estaticamente incorreto em alguns casos; as regras são **heurísticas** — revisão humana na fila `revisao_alteracao` continua recomendada para conteúdo sensível da marca.
- SVG e não-imagens são recusados na validação MIME antes da IA.
