Documentação Inclusa
This commit is contained in:
204
documentação/03_AI_VISION_LAYER.md
Normal file
204
documentação/03_AI_VISION_LAYER.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# 👁️ Camada de Visão por IA
|
||||
|
||||
## Objetivo
|
||||
|
||||
Usar IA de visão computacional para **identificar automaticamente** o que está na foto tirada pelo utilizador, sugerindo:
|
||||
- Nome do item
|
||||
- Categoria
|
||||
- Tags relevantes
|
||||
|
||||
Isto reduz o esforço do utilizador ao mínimo.
|
||||
|
||||
---
|
||||
|
||||
## Serviço escolhido: Google Vision API
|
||||
|
||||
### Porquê Google Vision?
|
||||
- Fácil integração com Firebase (mesmo ecosistema Google)
|
||||
- Excelente deteção de objetos do quotidiano
|
||||
- Labels em múltiplos idiomas
|
||||
- Tier gratuito generoso (1.000 unidades/mês)
|
||||
- Documentação extensa
|
||||
|
||||
### Funcionalidades usadas no MVP
|
||||
|
||||
| Feature | Uso |
|
||||
|---------|-----|
|
||||
| `LABEL_DETECTION` | Identificar o objeto (ex: "T-shirt", "Laptop") |
|
||||
| `OBJECT_LOCALIZATION` | Confirmar que existe um objeto na foto |
|
||||
|
||||
> No MVP **não** usamos OCR, Safe Search ou outras features avançadas.
|
||||
|
||||
---
|
||||
|
||||
## Integração técnica
|
||||
|
||||
### Chamada à API (exemplo React Native)
|
||||
|
||||
```javascript
|
||||
// services/visionApi.js
|
||||
|
||||
const VISION_API_KEY = process.env.GOOGLE_VISION_API_KEY;
|
||||
const VISION_API_URL = `https://vision.googleapis.com/v1/images:annotate?key=${VISION_API_KEY}`;
|
||||
|
||||
export async function analyzeImage(base64Image) {
|
||||
const requestBody = {
|
||||
requests: [
|
||||
{
|
||||
image: { content: base64Image },
|
||||
features: [
|
||||
{ type: 'LABEL_DETECTION', maxResults: 10 },
|
||||
{ type: 'OBJECT_LOCALIZATION', maxResults: 5 }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const response = await fetch(VISION_API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return parseVisionResponse(data);
|
||||
}
|
||||
```
|
||||
|
||||
### Parsing da resposta
|
||||
|
||||
```javascript
|
||||
// services/visionApi.js (continuação)
|
||||
|
||||
export function parseVisionResponse(data) {
|
||||
const labels = data.responses[0]?.labelAnnotations || [];
|
||||
const objects = data.responses[0]?.localizedObjectAnnotations || [];
|
||||
|
||||
// Extrair labels com score > 0.7
|
||||
const highConfidenceLabels = labels
|
||||
.filter(l => l.score > 0.7)
|
||||
.map(l => l.description.toLowerCase());
|
||||
|
||||
// Mapear para categoria da app
|
||||
const category = mapLabelsToCategory(highConfidenceLabels);
|
||||
|
||||
// Sugerir nome baseado no objeto mais confiante
|
||||
const suggestedName = objects[0]?.name || labels[0]?.description || 'Item';
|
||||
|
||||
return {
|
||||
suggestedName,
|
||||
category,
|
||||
tags: highConfidenceLabels.slice(0, 5),
|
||||
rawLabels: highConfidenceLabels
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mapeamento de Labels → Categorias
|
||||
|
||||
```javascript
|
||||
// constants/categoryMapping.js
|
||||
|
||||
export const LABEL_TO_CATEGORY = {
|
||||
// Roupa
|
||||
't-shirt': 'clothing',
|
||||
'shirt': 'clothing',
|
||||
'dress': 'clothing',
|
||||
'jacket': 'clothing',
|
||||
'coat': 'clothing',
|
||||
'jeans': 'clothing',
|
||||
'trousers': 'clothing',
|
||||
'sweater': 'clothing',
|
||||
'clothing': 'clothing',
|
||||
'fashion': 'clothing',
|
||||
|
||||
// Eletrónica
|
||||
'laptop': 'electronics',
|
||||
'computer': 'electronics',
|
||||
'smartphone': 'electronics',
|
||||
'tablet': 'electronics',
|
||||
'headphones': 'electronics',
|
||||
'camera': 'electronics',
|
||||
'charger': 'electronics',
|
||||
'cable': 'electronics',
|
||||
'electronics': 'electronics',
|
||||
|
||||
// Calçado
|
||||
'shoe': 'footwear',
|
||||
'boot': 'footwear',
|
||||
'sneaker': 'footwear',
|
||||
'sandal': 'footwear',
|
||||
'footwear': 'footwear',
|
||||
|
||||
// Acessórios
|
||||
'bag': 'accessories',
|
||||
'backpack': 'accessories',
|
||||
'watch': 'accessories',
|
||||
'sunglasses': 'accessories',
|
||||
'hat': 'accessories',
|
||||
'belt': 'accessories',
|
||||
|
||||
// Documentos
|
||||
'passport': 'documents',
|
||||
'document': 'documents',
|
||||
'book': 'documents',
|
||||
'notebook': 'documents',
|
||||
};
|
||||
|
||||
export function mapLabelsToCategory(labels) {
|
||||
for (const label of labels) {
|
||||
if (LABEL_TO_CATEGORY[label]) {
|
||||
return LABEL_TO_CATEGORY[label];
|
||||
}
|
||||
}
|
||||
return 'other'; // fallback
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fluxo de UX ao adicionar item
|
||||
|
||||
```
|
||||
1. Utilizador tira foto
|
||||
2. App mostra loading ("A analisar item...")
|
||||
3. Google Vision responde com labels
|
||||
4. App mostra:
|
||||
- Nome sugerido (editável)
|
||||
- Categoria sugerida (editável)
|
||||
- Tags sugeridas (checkboxes, editáveis)
|
||||
5. Utilizador confirma ou ajusta
|
||||
6. Item guardado
|
||||
```
|
||||
|
||||
> O utilizador tem **sempre** controlo final. A IA apenas sugere.
|
||||
|
||||
---
|
||||
|
||||
## Tratamento de erros
|
||||
|
||||
| Erro | Comportamento |
|
||||
|------|---------------|
|
||||
| API indisponível | Mostrar formulário manual, sem sugestões |
|
||||
| Foto sem objeto reconhecível | Mostrar sugestão "Outro" e pedir nome manual |
|
||||
| Score muito baixo (< 0.5) | Ignorar label, não sugerir |
|
||||
| Timeout | Retry 1x, depois formulário manual |
|
||||
|
||||
---
|
||||
|
||||
## Evolução futura desta camada
|
||||
|
||||
- Fase 2: Treinar modelo custom com os dados dos utilizadores
|
||||
- Fase 3: Identificar marcas e modelos específicos
|
||||
- Fase 3: Reconhecimento de cor dominante para filtros de outfit
|
||||
|
||||
---
|
||||
|
||||
## Notas para o agente IA
|
||||
|
||||
- A chave da API (`GOOGLE_VISION_API_KEY`) nunca deve aparecer em código client-side em produção — usar Firebase Cloud Functions como proxy
|
||||
- No MVP, a chamada pode ser feita diretamente do cliente para simplicidade
|
||||
- Nunca guardar `rawLabels` como `name` sem confirmação do utilizador
|
||||
- O mapeamento de labels é iterativo — quando encontrar labels não mapeadas, adicionar ao ficheiro `categoryMapping.js`
|
||||
Reference in New Issue
Block a user