Spaces:
Runtime error
Runtime error
sara.mesquita commited on
Commit ·
f9f4f9a
1
Parent(s): 0d61aae
Changes nemotron model
Browse files- README.md +22 -13
- core/ai.py +110 -70
README.md
CHANGED
|
@@ -16,13 +16,17 @@ tags:
|
|
| 16 |
- small-models
|
| 17 |
- gradio
|
| 18 |
- build-small-hackathon
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
- backyard-ai
|
| 20 |
- leaflet
|
| 21 |
- cosine-similarity
|
| 22 |
models:
|
| 23 |
-
- nvidia/nemotron-3-nano-omni-30b-a3b-reasoning
|
| 24 |
-
- sentence-transformers/all-MiniLM-L6-v2
|
| 25 |
- meta-llama/Llama-3.2-11B-Vision-Instruct
|
|
|
|
|
|
|
| 26 |
---
|
| 27 |
|
| 28 |
# PawMap 🐾 — Collaborative stray animal mapping with AI
|
|
@@ -34,6 +38,9 @@ Built for the [**Build Small Hackathon 2026**](https://huggingface.co/build-smal
|
|
| 34 |
|
| 35 |
**Try it:** [live Space](https://huggingface.co/spaces/build-small-hackathon/viralata-mapper) · **Traces:** [dataset on the Hub](https://huggingface.co/datasets/build-small-hackathon/viralata-mapper-storage)
|
| 36 |
|
|
|
|
|
|
|
|
|
|
| 37 |
---
|
| 38 |
|
| 39 |
## The Problem
|
|
@@ -54,10 +61,10 @@ There are two AI-powered flows: **registering a sighting** and **recording help*
|
|
| 54 |
|
| 55 |
```mermaid
|
| 56 |
flowchart TD
|
| 57 |
-
A["📷 Photo from phone"] --> B["🤖 Vision AI\nLlama-3.2-11B
|
| 58 |
B -->|"is it an animal?"| C{Animal detected?}
|
| 59 |
C -->|"No"| ERR["❌ Error message\n'No dog or cat identified'"]
|
| 60 |
-
C -->|"Yes"| D["📝 Structured JSON\nspecies · breed · color · condition · marks"]
|
| 61 |
D --> E["🔢 Semantic embedding\nall-MiniLM-L6-v2 · 384 dim\n(local, 22M params)"]
|
| 62 |
E --> F{Cosine similarity\n≥ 0.80?}
|
| 63 |
F -->|"Yes — known animal"| G["➕ New sighting\nadded to existing animal"]
|
|
@@ -99,14 +106,14 @@ The map uses color to signal urgency: **🟢 green** = dog · **🟠 orange** =
|
|
| 99 |
|
| 100 |
| Component | Model / Library | Where it runs |
|
| 101 |
|---|---|---|
|
| 102 |
-
| Visual identification | **
|
| 103 |
-
|
|
| 104 |
-
| Semantic embedding | **all-MiniLM-L6-v2** (384 dim) | CPU, local |
|
| 105 |
| Animal matching | Cosine similarity (threshold 0.80) | CPU, local |
|
| 106 |
| Database | SQLite | local / persistent |
|
| 107 |
| Frontend | SPA via `gradio.Server` + Leaflet.js + Lucide Icons | browser |
|
| 108 |
|
| 109 |
-
**Total parameters running locally: ~22M
|
| 110 |
|
| 111 |
---
|
| 112 |
|
|
@@ -129,6 +136,8 @@ This submission earns the following hackathon bonus quests:
|
|
| 129 |
|
| 130 |
- 🎨 **Off-Brand** — Fully custom interface via `gradio.Server`, no default Gradio components visible. SPA frontend with Leaflet.js for the map, Lucide Icons, and custom design.
|
| 131 |
- 📡 **Sharing is Caring** — Full traces of every sighting (photo → AI analysis → embedding → matching result) are automatically published as a dataset on the Hub via `/admin/push-traces`.
|
|
|
|
|
|
|
| 132 |
|
| 133 |
---
|
| 134 |
|
|
@@ -139,10 +148,10 @@ git clone https://huggingface.co/spaces/build-small-hackathon/viralata-mapper
|
|
| 139 |
cd viralata-mapper
|
| 140 |
pip install -r requirements.txt
|
| 141 |
|
| 142 |
-
#
|
| 143 |
HF_TOKEN=hf_... python app.py
|
| 144 |
|
| 145 |
-
#
|
| 146 |
NVIDIA_API_KEY=nvapi_... python app.py
|
| 147 |
|
| 148 |
# no key — runs in fallback mode without AI identification
|
|
@@ -162,7 +171,7 @@ On first run the app seeds the database with demo animals around Brasília so th
|
|
| 162 |
| Secret | Description |
|
| 163 |
|---|---|
|
| 164 |
| `HF_TOKEN` | HuggingFace token for Llama-3.2-11B-Vision via Serverless Inference |
|
| 165 |
-
| `NVIDIA_API_KEY` |
|
| 166 |
| `MATCH_THRESHOLD` | Optional. Similarity threshold. Default: `0.80` |
|
| 167 |
| `HF_DATASET_ID` | Optional. Hub dataset for trace publishing (e.g. `org/dataset-name`) |
|
| 168 |
|
|
@@ -214,7 +223,7 @@ Set up a **Persistent Storage Bucket** in the Space and mount it at `/data/` so
|
|
| 214 |
## Credits
|
| 215 |
|
| 216 |
- **[Meta](https://ai.meta.com/llama/)** — Llama 3.2 11B Vision Instruct (Llama 3.2 Community License).
|
| 217 |
-
- **[NVIDIA](https://build.nvidia.com/)** — Nemotron
|
| 218 |
- **[Hugging Face](https://huggingface.co/)** — `sentence-transformers/all-MiniLM-L6-v2`, Serverless Inference, Space hosting.
|
| 219 |
- **[Leaflet.js](https://leafletjs.com/)** — interactive map.
|
| 220 |
- **[Gradio](https://gradio.app/)** — `gradio.Server` for the custom frontend.
|
|
@@ -224,7 +233,7 @@ Set up a **Persistent Storage Bucket** in the Space and mount it at `/data/` so
|
|
| 224 |
|
| 225 |
## License
|
| 226 |
|
| 227 |
-
MIT for the application code. Models follow their own licenses — see the Meta (Llama 3.2) and NVIDIA (Nemotron
|
| 228 |
|
| 229 |
---
|
| 230 |
|
|
|
|
| 16 |
- small-models
|
| 17 |
- gradio
|
| 18 |
- build-small-hackathon
|
| 19 |
+
- track:backyard
|
| 20 |
+
- sponsor:nvidia
|
| 21 |
+
- achievement:offbrand
|
| 22 |
+
- achievement:sharing
|
| 23 |
- backyard-ai
|
| 24 |
- leaflet
|
| 25 |
- cosine-similarity
|
| 26 |
models:
|
|
|
|
|
|
|
| 27 |
- meta-llama/Llama-3.2-11B-Vision-Instruct
|
| 28 |
+
- nvidia/Nemotron-Mini-4B-Instruct
|
| 29 |
+
- sentence-transformers/all-MiniLM-L6-v2
|
| 30 |
---
|
| 31 |
|
| 32 |
# PawMap 🐾 — Collaborative stray animal mapping with AI
|
|
|
|
| 38 |
|
| 39 |
**Try it:** [live Space](https://huggingface.co/spaces/build-small-hackathon/viralata-mapper) · **Traces:** [dataset on the Hub](https://huggingface.co/datasets/build-small-hackathon/viralata-mapper-storage)
|
| 40 |
|
| 41 |
+
> 🎬 **Demo video:** `TODO — paste your YouTube / Vimeo / Loom link here before submitting`
|
| 42 |
+
> 📣 **Social post:** `TODO — paste your X / LinkedIn / Bluesky / Mastodon post link here before submitting`
|
| 43 |
+
|
| 44 |
---
|
| 45 |
|
| 46 |
## The Problem
|
|
|
|
| 61 |
|
| 62 |
```mermaid
|
| 63 |
flowchart TD
|
| 64 |
+
A["📷 Photo from phone"] --> B["🤖 Vision AI\nLlama-3.2-11B-Vision\n(HF Serverless)"]
|
| 65 |
B -->|"is it an animal?"| C{Animal detected?}
|
| 66 |
C -->|"No"| ERR["❌ Error message\n'No dog or cat identified'"]
|
| 67 |
+
C -->|"Yes"| D["📝 Structured JSON\nNemotron Mini 4B Instruct normalizes\nspecies · breed · color · condition · marks"]
|
| 68 |
D --> E["🔢 Semantic embedding\nall-MiniLM-L6-v2 · 384 dim\n(local, 22M params)"]
|
| 69 |
E --> F{Cosine similarity\n≥ 0.80?}
|
| 70 |
F -->|"Yes — known animal"| G["➕ New sighting\nadded to existing animal"]
|
|
|
|
| 106 |
|
| 107 |
| Component | Model / Library | Where it runs |
|
| 108 |
|---|---|---|
|
| 109 |
+
| Visual identification | **Llama-3.2-11B-Vision-Instruct** (11B) via HF Serverless | remote API |
|
| 110 |
+
| Text reasoning / JSON | **Nemotron Mini 4B Instruct** via NVIDIA NIM | remote API |
|
| 111 |
+
| Semantic embedding | **all-MiniLM-L6-v2** (384 dim, 22M params) | CPU, local |
|
| 112 |
| Animal matching | Cosine similarity (threshold 0.80) | CPU, local |
|
| 113 |
| Database | SQLite | local / persistent |
|
| 114 |
| Frontend | SPA via `gradio.Server` + Leaflet.js + Lucide Icons | browser |
|
| 115 |
|
| 116 |
+
**Total parameters running locally: ~22M** (MiniLM). The vision and reasoning models run via serverless API — nothing heavy on the Space machine. **Every model used is well under the 32B hackathon cap:** Llama-3.2-11B-Vision (11B), Nemotron Mini 4B Instruct (4B) and MiniLM-L6-v2 (22M).
|
| 117 |
|
| 118 |
---
|
| 119 |
|
|
|
|
| 136 |
|
| 137 |
- 🎨 **Off-Brand** — Fully custom interface via `gradio.Server`, no default Gradio components visible. SPA frontend with Leaflet.js for the map, Lucide Icons, and custom design.
|
| 138 |
- 📡 **Sharing is Caring** — Full traces of every sighting (photo → AI analysis → embedding → matching result) are automatically published as a dataset on the Hub via `/admin/push-traces`.
|
| 139 |
+
- 🤏 **Tiny Titan** — The only on-device model is `all-MiniLM-L6-v2` (22M params, far under 4B): all animal matching runs locally on a tiny embedding model.
|
| 140 |
+
- 🟩 **NVIDIA / Nemotron** — Structured identification is normalized by `Nemotron-Mini-4B-Instruct` via NVIDIA NIM — small (4B), well under the 32B cap.
|
| 141 |
|
| 142 |
---
|
| 143 |
|
|
|
|
| 148 |
cd viralata-mapper
|
| 149 |
pip install -r requirements.txt
|
| 150 |
|
| 151 |
+
# Vision via HuggingFace Serverless (Llama-3.2-11B-Vision)
|
| 152 |
HF_TOKEN=hf_... python app.py
|
| 153 |
|
| 154 |
+
# Add NVIDIA NIM for Nemotron Mini 4B Instruct (structured-output normalization)
|
| 155 |
NVIDIA_API_KEY=nvapi_... python app.py
|
| 156 |
|
| 157 |
# no key — runs in fallback mode without AI identification
|
|
|
|
| 171 |
| Secret | Description |
|
| 172 |
|---|---|
|
| 173 |
| `HF_TOKEN` | HuggingFace token for Llama-3.2-11B-Vision via Serverless Inference |
|
| 174 |
+
| `NVIDIA_API_KEY` | NVIDIA NIM key for Nemotron Mini 4B Instruct (structured-output normalization) |
|
| 175 |
| `MATCH_THRESHOLD` | Optional. Similarity threshold. Default: `0.80` |
|
| 176 |
| `HF_DATASET_ID` | Optional. Hub dataset for trace publishing (e.g. `org/dataset-name`) |
|
| 177 |
|
|
|
|
| 223 |
## Credits
|
| 224 |
|
| 225 |
- **[Meta](https://ai.meta.com/llama/)** — Llama 3.2 11B Vision Instruct (Llama 3.2 Community License).
|
| 226 |
+
- **[NVIDIA](https://build.nvidia.com/)** — Nemotron Mini 4B Instruct via NVIDIA NIM.
|
| 227 |
- **[Hugging Face](https://huggingface.co/)** — `sentence-transformers/all-MiniLM-L6-v2`, Serverless Inference, Space hosting.
|
| 228 |
- **[Leaflet.js](https://leafletjs.com/)** — interactive map.
|
| 229 |
- **[Gradio](https://gradio.app/)** — `gradio.Server` for the custom frontend.
|
|
|
|
| 233 |
|
| 234 |
## License
|
| 235 |
|
| 236 |
+
MIT for the application code. Models follow their own licenses — see the Meta (Llama 3.2) and NVIDIA (Nemotron Mini 4B Instruct) model cards for full terms.
|
| 237 |
|
| 238 |
---
|
| 239 |
|
core/ai.py
CHANGED
|
@@ -1,12 +1,21 @@
|
|
| 1 |
"""
|
| 2 |
-
ai.py — Visao via HuggingFace InferenceClient
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
| 10 |
"""
|
| 11 |
import base64
|
| 12 |
import io
|
|
@@ -20,7 +29,9 @@ import numpy as np
|
|
| 20 |
log = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
_HF_MODEL = "meta-llama/Llama-3.2-11B-Vision-Instruct"
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
|
| 25 |
PROMPT = (
|
| 26 |
"Examine this image carefully.\n"
|
|
@@ -47,41 +58,56 @@ PROMPT = (
|
|
| 47 |
" \"description_text\": \"one concise English sentence describing this specific animal for identity matching\"}"
|
| 48 |
)
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
class AnimalAI:
|
| 52 |
def __init__(self):
|
| 53 |
-
self.
|
| 54 |
-
self.
|
| 55 |
-
self.
|
|
|
|
| 56 |
|
| 57 |
hf_token = os.environ.get("HF_TOKEN", "")
|
| 58 |
nvidia_key = os.environ.get("NVIDIA_API_KEY", "")
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
if nvidia_key:
|
| 61 |
try:
|
| 62 |
from openai import OpenAI
|
| 63 |
-
self.
|
| 64 |
-
self.
|
| 65 |
-
self.client = OpenAI(
|
| 66 |
base_url="https://integrate.api.nvidia.com/v1",
|
| 67 |
api_key=nvidia_key,
|
| 68 |
)
|
| 69 |
-
log.info("
|
| 70 |
-
except ImportError:
|
| 71 |
-
log.warning("openai nao instalado")
|
| 72 |
-
|
| 73 |
-
elif hf_token:
|
| 74 |
-
try:
|
| 75 |
-
from huggingface_hub import InferenceClient
|
| 76 |
-
self.mode = "hf"
|
| 77 |
-
self.model = os.environ.get("NVIDIA_MODEL", _HF_MODEL)
|
| 78 |
-
self.client = InferenceClient(model=self.model, token=hf_token)
|
| 79 |
-
log.info("AI: %s via HF InferenceClient", self.model)
|
| 80 |
except ImportError:
|
| 81 |
-
log.warning("
|
| 82 |
-
|
| 83 |
-
else:
|
| 84 |
-
log.warning("Sem chave de API — IA desabilitada. Configure HF_TOKEN nos Secrets.")
|
| 85 |
|
| 86 |
self.embedder = None
|
| 87 |
try:
|
|
@@ -93,50 +119,26 @@ class AnimalAI:
|
|
| 93 |
|
| 94 |
def analyze_image(self, image) -> dict:
|
| 95 |
"""Analisa imagem. _ai_success=False indica que a IA nao foi usada."""
|
| 96 |
-
if self.
|
| 97 |
return self._fallback()
|
| 98 |
|
| 99 |
try:
|
| 100 |
img_b64 = self._to_b64(image)
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
"
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
else:
|
| 117 |
-
# NVIDIA NIM — Nemotron Omni (reasoning model)
|
| 118 |
-
resp = self.client.chat.completions.create(
|
| 119 |
-
model=self.model,
|
| 120 |
-
messages=[{
|
| 121 |
-
"role": "user",
|
| 122 |
-
"content": [
|
| 123 |
-
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64," + img_b64}},
|
| 124 |
-
{"type": "text", "text": PROMPT},
|
| 125 |
-
],
|
| 126 |
-
}],
|
| 127 |
-
max_tokens=1024,
|
| 128 |
-
temperature=0.6,
|
| 129 |
-
top_p=0.95,
|
| 130 |
-
extra_body={
|
| 131 |
-
"chat_template_kwargs": {"enable_thinking": True},
|
| 132 |
-
"reasoning_budget": 512,
|
| 133 |
-
},
|
| 134 |
-
)
|
| 135 |
-
msg = resp.choices[0].message
|
| 136 |
-
# Nemotron Omni: resposta pode estar em content ou reasoning_content
|
| 137 |
-
raw = (msg.content or "") or (getattr(msg, "reasoning_content", "") or "")
|
| 138 |
-
|
| 139 |
-
log.info("AI resposta: %s", raw[:200])
|
| 140 |
result = self._parse(raw)
|
| 141 |
|
| 142 |
# Rejeição explícita: a IA não viu nenhum animal
|
|
@@ -144,14 +146,44 @@ class AnimalAI:
|
|
| 144 |
log.info("IA: nenhum animal detectado na imagem.")
|
| 145 |
return {"is_animal": False, "_ai_success": True}
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
result["is_animal"] = True
|
| 148 |
result["_ai_success"] = True
|
| 149 |
return result
|
| 150 |
|
| 151 |
except Exception as e:
|
| 152 |
-
log.error("Erro na API de
|
| 153 |
return self._fallback()
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
def get_embedding(self, description: dict) -> list:
|
| 156 |
"""Embedding da descricao. Aleatorio se IA falhou (evita falsos matches)."""
|
| 157 |
if not description.get("_ai_success", True):
|
|
@@ -177,13 +209,21 @@ class AnimalAI:
|
|
| 177 |
return base64.b64encode(buf.getvalue()).decode()
|
| 178 |
|
| 179 |
@staticmethod
|
| 180 |
-
def
|
|
|
|
| 181 |
m = re.search(r"\{.*\}", raw, re.DOTALL)
|
| 182 |
if m:
|
| 183 |
try:
|
| 184 |
return json.loads(m.group())
|
| 185 |
except json.JSONDecodeError:
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
log.warning("JSON nao parseado — fallback")
|
| 188 |
return AnimalAI._fallback()
|
| 189 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
ai.py — Visao via HuggingFace InferenceClient + normalizacao via NVIDIA NIM.
|
| 3 |
|
| 4 |
+
Arquitetura (todos os modelos < 32B):
|
| 5 |
+
- Visao : meta-llama/Llama-3.2-11B-Vision-Instruct (11B, HF Serverless) -> precisa HF_TOKEN
|
| 6 |
+
- Texto : nvidia/nemotron-mini-4b-instruct (4B, NVIDIA NIM) -> precisa NVIDIA_API_KEY (opcional)
|
| 7 |
+
- Embed : sentence-transformers/all-MiniLM-L6-v2 (22M, local/CPU)
|
| 8 |
+
|
| 9 |
+
O Llama enxerga a foto e devolve a descricao bruta. Se NVIDIA_API_KEY estiver
|
| 10 |
+
configurada, o Nemotron Mini 4B Instruct (texto puro) normaliza esse JSON para o schema
|
| 11 |
+
estrito (raca da lista permitida, cores simples, condicao, frase de matching).
|
| 12 |
+
Sem NVIDIA_API_KEY, usa-se direto a saida do Llama.
|
| 13 |
|
| 14 |
+
Variaveis de ambiente:
|
| 15 |
+
HF_TOKEN — token HuggingFace (HF Serverless Inference) — obrigatorio p/ visao
|
| 16 |
+
NVIDIA_API_KEY — chave NVIDIA NIM (free tier em build.nvidia.com) — opcional
|
| 17 |
+
HF_VISION_MODEL — override do modelo de visao (default Llama-3.2-11B-Vision)
|
| 18 |
+
NVIDIA_MODEL — override do modelo NIM (default nvidia/nemotron-mini-4b-instruct)
|
| 19 |
"""
|
| 20 |
import base64
|
| 21 |
import io
|
|
|
|
| 29 |
log = logging.getLogger(__name__)
|
| 30 |
|
| 31 |
_HF_MODEL = "meta-llama/Llama-3.2-11B-Vision-Instruct"
|
| 32 |
+
# String do modelo na API NVIDIA NIM (integrate.api.nvidia.com).
|
| 33 |
+
# Confirme o id exato em build.nvidia.com; pode sobrescrever via NVIDIA_MODEL.
|
| 34 |
+
_NIM_MODEL = "nvidia/nemotron-mini-4b-instruct" # Nemotron Mini 4B Instruct (texto, 4B)
|
| 35 |
|
| 36 |
PROMPT = (
|
| 37 |
"Examine this image carefully.\n"
|
|
|
|
| 58 |
" \"description_text\": \"one concise English sentence describing this specific animal for identity matching\"}"
|
| 59 |
)
|
| 60 |
|
| 61 |
+
# Nemotron Mini 4B Instruct normaliza a saida da visao para o schema estrito (texto puro).
|
| 62 |
+
NORMALIZE_PROMPT = (
|
| 63 |
+
"You normalize raw animal descriptions for a stray-animal database.\n"
|
| 64 |
+
"Given the input JSON, return ONLY a cleaned JSON object with the SAME keys.\n"
|
| 65 |
+
"Rules:\n"
|
| 66 |
+
"- breed_estimate must be the single best match from the allowed dog/cat breed lists; "
|
| 67 |
+
"use SRD (dog) or Domestic Shorthair (cat) only if truly unidentifiable.\n"
|
| 68 |
+
"- species is 'dog' or 'cat'; size is 'small', 'medium' or 'large'.\n"
|
| 69 |
+
"- primary_color and secondary_colors are simple lowercase words.\n"
|
| 70 |
+
"- condition is exactly one of: healthy, thin, injured.\n"
|
| 71 |
+
"- description_text is ONE concise English sentence for identity matching.\n"
|
| 72 |
+
"- Do NOT invent details not present in the input. No markdown, no explanation.\n\n"
|
| 73 |
+
"Input JSON:\n"
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
|
| 77 |
class AnimalAI:
|
| 78 |
def __init__(self):
|
| 79 |
+
self.vision_model = _HF_MODEL
|
| 80 |
+
self.vision_client = None # InferenceClient (HF) — visao
|
| 81 |
+
self.nim_model = _NIM_MODEL
|
| 82 |
+
self.nim_client = None # OpenAI client -> NVIDIA NIM — texto
|
| 83 |
|
| 84 |
hf_token = os.environ.get("HF_TOKEN", "")
|
| 85 |
nvidia_key = os.environ.get("NVIDIA_API_KEY", "")
|
| 86 |
|
| 87 |
+
# Visao: Llama-3.2-11B-Vision via HF Serverless (obrigatorio para identificar foto)
|
| 88 |
+
if hf_token:
|
| 89 |
+
try:
|
| 90 |
+
from huggingface_hub import InferenceClient
|
| 91 |
+
self.vision_model = os.environ.get("HF_VISION_MODEL", _HF_MODEL)
|
| 92 |
+
self.vision_client = InferenceClient(model=self.vision_model, token=hf_token)
|
| 93 |
+
log.info("Visao: %s via HF InferenceClient", self.vision_model)
|
| 94 |
+
except ImportError:
|
| 95 |
+
log.warning("huggingface_hub nao instalado")
|
| 96 |
+
else:
|
| 97 |
+
log.warning("Sem HF_TOKEN — visao desabilitada. Configure HF_TOKEN nos Secrets.")
|
| 98 |
+
|
| 99 |
+
# Texto: Nemotron Mini 4B Instruct via NVIDIA NIM (opcional — normaliza o JSON)
|
| 100 |
if nvidia_key:
|
| 101 |
try:
|
| 102 |
from openai import OpenAI
|
| 103 |
+
self.nim_model = os.environ.get("NVIDIA_MODEL", _NIM_MODEL)
|
| 104 |
+
self.nim_client = OpenAI(
|
|
|
|
| 105 |
base_url="https://integrate.api.nvidia.com/v1",
|
| 106 |
api_key=nvidia_key,
|
| 107 |
)
|
| 108 |
+
log.info("Texto: Nemotron Mini 4B Instruct via NVIDIA NIM (%s)", self.nim_model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
except ImportError:
|
| 110 |
+
log.warning("openai nao instalado — normalizacao Nemotron desabilitada")
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
self.embedder = None
|
| 113 |
try:
|
|
|
|
| 119 |
|
| 120 |
def analyze_image(self, image) -> dict:
|
| 121 |
"""Analisa imagem. _ai_success=False indica que a IA nao foi usada."""
|
| 122 |
+
if self.vision_client is None:
|
| 123 |
return self._fallback()
|
| 124 |
|
| 125 |
try:
|
| 126 |
img_b64 = self._to_b64(image)
|
| 127 |
|
| 128 |
+
# Visao multimodal — Llama-3.2-11B-Vision (HF InferenceClient)
|
| 129 |
+
resp = self.vision_client.chat_completion(
|
| 130 |
+
messages=[{
|
| 131 |
+
"role": "user",
|
| 132 |
+
"content": [
|
| 133 |
+
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64," + img_b64}},
|
| 134 |
+
{"type": "text", "text": PROMPT},
|
| 135 |
+
],
|
| 136 |
+
}],
|
| 137 |
+
max_tokens=400,
|
| 138 |
+
temperature=0.1,
|
| 139 |
+
)
|
| 140 |
+
raw = resp.choices[0].message.content or ""
|
| 141 |
+
log.info("Visao resposta: %s", raw[:200])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
result = self._parse(raw)
|
| 143 |
|
| 144 |
# Rejeição explícita: a IA não viu nenhum animal
|
|
|
|
| 146 |
log.info("IA: nenhum animal detectado na imagem.")
|
| 147 |
return {"is_animal": False, "_ai_success": True}
|
| 148 |
|
| 149 |
+
# Normalização opcional via Nemotron Mini 4B Instruct (texto puro, NVIDIA NIM)
|
| 150 |
+
if self.nim_client is not None:
|
| 151 |
+
result = self._normalize_with_nemotron(result)
|
| 152 |
+
|
| 153 |
result["is_animal"] = True
|
| 154 |
result["_ai_success"] = True
|
| 155 |
return result
|
| 156 |
|
| 157 |
except Exception as e:
|
| 158 |
+
log.error("Erro na API de visao: %s", e)
|
| 159 |
return self._fallback()
|
| 160 |
|
| 161 |
+
def _normalize_with_nemotron(self, result: dict) -> dict:
|
| 162 |
+
"""Coage o JSON da visao para o schema estrito usando Nemotron Mini 4B Instruct (texto)."""
|
| 163 |
+
try:
|
| 164 |
+
payload = json.dumps(result, ensure_ascii=False)
|
| 165 |
+
resp = self.nim_client.chat.completions.create(
|
| 166 |
+
model=self.nim_model,
|
| 167 |
+
messages=[{"role": "user", "content": NORMALIZE_PROMPT + payload}],
|
| 168 |
+
max_tokens=400,
|
| 169 |
+
temperature=0.2,
|
| 170 |
+
top_p=0.9,
|
| 171 |
+
)
|
| 172 |
+
msg = resp.choices[0].message
|
| 173 |
+
raw = (msg.content or "") or (getattr(msg, "reasoning_content", "") or "")
|
| 174 |
+
cleaned = self._extract_json(raw)
|
| 175 |
+
if cleaned:
|
| 176 |
+
for k in ("species", "breed_estimate", "size", "primary_color",
|
| 177 |
+
"secondary_colors", "distinctive_marks", "condition",
|
| 178 |
+
"description_text"):
|
| 179 |
+
if k in cleaned and cleaned[k] not in (None, "", []):
|
| 180 |
+
result[k] = cleaned[k]
|
| 181 |
+
log.info("Nemotron 4B normalizou os campos.")
|
| 182 |
+
return result
|
| 183 |
+
except Exception as e:
|
| 184 |
+
log.warning("Nemotron normalize falhou, usando saida da visao: %s", e)
|
| 185 |
+
return result
|
| 186 |
+
|
| 187 |
def get_embedding(self, description: dict) -> list:
|
| 188 |
"""Embedding da descricao. Aleatorio se IA falhou (evita falsos matches)."""
|
| 189 |
if not description.get("_ai_success", True):
|
|
|
|
| 209 |
return base64.b64encode(buf.getvalue()).decode()
|
| 210 |
|
| 211 |
@staticmethod
|
| 212 |
+
def _extract_json(raw: str):
|
| 213 |
+
"""Retorna dict do primeiro bloco JSON valido, ou None."""
|
| 214 |
m = re.search(r"\{.*\}", raw, re.DOTALL)
|
| 215 |
if m:
|
| 216 |
try:
|
| 217 |
return json.loads(m.group())
|
| 218 |
except json.JSONDecodeError:
|
| 219 |
+
return None
|
| 220 |
+
return None
|
| 221 |
+
|
| 222 |
+
@staticmethod
|
| 223 |
+
def _parse(raw: str) -> dict:
|
| 224 |
+
parsed = AnimalAI._extract_json(raw)
|
| 225 |
+
if parsed is not None:
|
| 226 |
+
return parsed
|
| 227 |
log.warning("JSON nao parseado — fallback")
|
| 228 |
return AnimalAI._fallback()
|
| 229 |
|