Metadata-Version: 2.4
Name: ncm-classificador
Version: 0.1.0
Summary: Classificador fiscal NCM: top-3 + confiança calibrada + abstenção, offline na sua CPU
Project-URL: Modelo, https://huggingface.co/DominuZ/ncm-classificador-bertimbau
Project-URL: Benchmark, https://huggingface.co/datasets/DominuZ/rfb-bench
Author: DominuZ
License-Expression: MIT
License-File: LICENSE
Keywords: bert,cclasstrib,classificacao-fiscal,ncm,nfe,nota-fiscal,onnx,reforma-tributaria
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Natural Language :: Portuguese (Brazilian)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Accounting
Requires-Python: >=3.12
Requires-Dist: numpy>=2.0
Requires-Dist: onnxruntime>=1.20
Requires-Dist: rich>=13.9
Requires-Dist: tokenizers>=0.20
Provides-Extra: api
Requires-Dist: fastapi>=0.115; extra == 'api'
Requires-Dist: uvicorn>=0.32; extra == 'api'
Description-Content-Type: text/markdown

# ncm-classificador

Classificador fiscal NCM: descrição livre de item de NF-e → top-3 códigos NCM com
confiança calibrada e abstenção. Roda offline, na sua CPU — nenhum dado sai da máquina.

> **Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação
> é do contribuinte/contador.**

---

## Instalação

### Via pip (com suporte à API)

```bash
pip install ncm-classificador[api]
```

Sem a flag `[api]`, instala apenas a CLI (sem dependências do servidor FastAPI).

---

## Uso

A CLI resolve o pacote-modelo nesta ordem: `--pacote DIR` > env `NCM_PACOTE` > `~/.ncm/pacote`.

### 1. Classificar item único

```bash
ncm classificar "PARAF SEXT ZINC M8X40 DIN933"
```

**Saída real** (tabela `rich` — código + confiança calibrada; a descrição textual do NCM
não faz parte do pacote exportado):
```
                    NCM para: PARAF SEXT ZINC M8X40 DIN933
┌───────────────┬─────────────────────┐
│ candidato NCM │ confiança calibrada │
├───────────────┼─────────────────────┤
│ 73181500      │               98.3% │
│ 73181100      │                0.8% │
│ 86079900      │                0.1% │
└───────────────┴─────────────────────┘
Ferramenta de apoio · caráter orientativo · a responsabilidade pela
classificação é do contribuinte/contador.
```

Quando o item abstém, uma linha extra em destaque aparece antes do disclaimer:
`ABSTEVE — confiança insuficiente; escale a um contador`.

Com `--json`, a saída é uma linha NDJSON (mesmo shape usado pela API — ver adiante):
```bash
ncm classificar "PARAF SEXT ZINC M8X40 DIN933" --json
```
```json
{"descricao": "PARAF SEXT ZINC M8X40 DIN933", "top3": [{"ncm8": "73181500", "confianca": 0.983291}, {"ncm8": "73181100", "confianca": 0.008137}, {"ncm8": "86079900", "confianca": 0.001092}], "abstem": false, "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.", "versao_pacote": "d-1"}
```

### 2. Classificar lote (arquivo CSV)

```bash
ncm lote itens.csv > resultado.csv
```

Não existe `--saida`: o resultado sai no stdout e é redirecionado com `>`. (`--json`
também é aceito e emite uma linha NDJSON por item, no mesmo formato do item 1.)

**Entrada (itens.csv, coluna obrigatória `descricao`):**
```
descricao
PARAF SEXT ZINC M8X40 DIN933
PORCAS M8 ISO 4032
```

**Saída real (resultado.csv)** — colunas `ncm8_N`/`confianca_N` (não `ncm_N`), campo
`abstem` (não `abstencao`), e rodapé com o disclaimer:
```
descricao,ncm8_1,confianca_1,ncm8_2,confianca_2,ncm8_3,confianca_3,abstem
PARAF SEXT ZINC M8X40 DIN933,73181500,0.983291,73181100,0.008137,86079900,0.001092,false
PORCAS M8 ISO 4032,73229090,0.352457,73181600,0.033459,73012000,0.017840,true
# Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.
```

Note que a segunda linha abstém (`abstem=true`, confiança do 1º candidato abaixo do
piso) — a saída real, não um exemplo forjado.

### 3. Consultar informações do modelo

```bash
ncm info
```

**Saída real:**
```
                      Pacote de inferência NCM
┌──────────────────────────┬────────────────────────────────────────┐
│ campo                    │ valor                                  │
├──────────────────────────┼────────────────────────────────────────┤
│ versão do pacote         │ d-1                                    │
│ modelo base              │ neuralmind/bert-large-portuguese-cased │
│ corrida / data de export │ d / 2026-07-17                         │
│ temperatura (T)          │ 0.786                                  │
│ pisos de abstenção       │ conf 0.55 · margem 0.1                 │
│ classes (folhas NCM)     │ 9748                                   │
│ integridade sha256       │ ok (conferida ao carregar)             │
└──────────────────────────┴────────────────────────────────────────┘
Ferramenta de apoio · caráter orientativo · a responsabilidade pela
classificação é do contribuinte/contador.
```

`ncm info` mostra proveniência e configuração de calibração do pacote carregado — não
expõe métricas de acurácia/ECE do conjunto de validação (essas vivem nos artefatos de
avaliação do treino, não no pacote de produto).

---

## Servidor API

### Iniciar o servidor

```bash
ncm servir --host 0.0.0.0 --porta 8000
```

### Variáveis de ambiente obrigatórias/opcionais

| Variável | Tipo | Padrão | Descrição |
|----------|------|--------|-----------|
| `NCM_API_KEYS` | string | (obrigatória) | Chave(s) de API separadas por `,` ex: `chave1,chave2`. Sem isto o servidor não sobe (fail-closed) |
| `NCM_RATE_LIMIT_RPM` | int | 120 | Limite de requisições por minuto por chave (token bucket em memória; zera no restart) |
| `NCM_PACOTE` | path | `~/.ncm/pacote` | Caminho até o diretório do pacote-modelo (no Docker, tipicamente `/pacote`, ver seção Docker) |

### Endpoints

#### `POST /v1/classificar`
Classifica um item único.

**Request:**
```bash
curl -X POST http://localhost:8000/v1/classificar \
  -H "X-API-Key: sua-chave-api" \
  -H "Content-Type: application/json" \
  -d '{"descricao": "PARAF SEXT ZINC M8X40 DIN933"}'
```

**Response (200)** — mesmo shape do `--json` da CLI (carga real, capturada rodando o
servidor local):
```json
{
  "descricao": "PARAF SEXT ZINC M8X40 DIN933",
  "top3": [
    {"ncm8": "73181500", "confianca": 0.983291},
    {"ncm8": "73181100", "confianca": 0.008137},
    {"ncm8": "86079900", "confianca": 0.001092}
  ],
  "abstem": false,
  "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
  "versao_pacote": "d-1"
}
```

`descricao` aceita 1 a 2000 caracteres. Corpo fora desse contrato (campo errado, string
vazia/longa demais) devolve `422` no formato `problem+json` (RFC 9457).

#### `POST /v1/classificar-lote`
Classifica múltiplos itens (máx. 200 por requisição). O corpo é `{"descricoes": [...]}`
— uma lista de strings, **não** uma lista de objetos `{"descricao": ...}`.

**Request:**
```bash
curl -X POST http://localhost:8000/v1/classificar-lote \
  -H "X-API-Key: sua-chave-api" \
  -H "Content-Type: application/json" \
  -d '{
    "descricoes": [
      "PARAF SEXT ZINC M8X40 DIN933",
      "PORCAS M8 ISO 4032"
    ]
  }'
```

**Response (200)** — envelope com `disclaimer`/`versao_pacote` no topo, e cada resultado
com o mesmo shape do endpoint acima (também carregando `disclaimer`/`versao_pacote`):
```json
{
  "resultados": [
    {
      "descricao": "PARAF SEXT ZINC M8X40 DIN933",
      "top3": [
        {"ncm8": "73181500", "confianca": 0.983291},
        {"ncm8": "73181100", "confianca": 0.008137},
        {"ncm8": "86079900", "confianca": 0.001092}
      ],
      "abstem": false,
      "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
      "versao_pacote": "d-1"
    },
    {
      "descricao": "PORCAS M8 ISO 4032",
      "top3": [
        {"ncm8": "73229090", "confianca": 0.352457},
        {"ncm8": "73181600", "confianca": 0.033459},
        {"ncm8": "73012000", "confianca": 0.017840}
      ],
      "abstem": true,
      "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
      "versao_pacote": "d-1"
    }
  ],
  "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.",
  "versao_pacote": "d-1"
}
```

O custo no rate limit é `len(descricoes)` tokens do balde — um lote de 50 itens custa 50,
não 1.

#### `GET /v1/info`
Retorna proveniência e configuração de calibração do pacote carregado. Não expõe
métricas de acurácia/ECE (não fazem parte do pacote de produto — ver seção `ncm info`
acima).

**Request:**
```bash
curl -H "X-API-Key: sua-chave-api" http://localhost:8000/v1/info
```

**Response (200)** — capturada rodando o servidor local com o pacote `d-1`:
```json
{
  "versao_pacote": "d-1",
  "modelo_base": "neuralmind/bert-large-portuguese-cased",
  "corrida": "d",
  "data_export": "2026-07-17",
  "temperatura": 0.7862830393715502,
  "piso_confianca": 0.55,
  "piso_margem": 0.1,
  "classes": 9748,
  "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador."
}
```

#### `GET /healthz`
Verifica saúde do servidor (usado pelo Docker HEALTHCHECK). Não exige `X-API-Key`.

**Request:**
```bash
curl http://localhost:8000/healthz
```

**Response (200):**
```json
{"status": "ok"}
```

---

## Docker

### Build

Deve ser executado **na raiz do repositório** (o Dockerfile copia o wheel de `dist/`):

```bash
# 1. Construir o wheel
uv build packages/ncm-classificador

# 2. Construir a imagem Docker
docker build -f packages/ncm-classificador/Dockerfile -t ncm-classificador .
```

### Run

O pacote-modelo (1,3 GB) **não é embarcado** na imagem — entra como volume:

```bash
docker run \
  -p 8000:8000 \
  -v /caminho/local/do/pacote:/pacote:ro \
  -e NCM_API_KEYS=sua-chave-api \
  -e NCM_RATE_LIMIT_RPM=120 \
  ncm-classificador
```

**Notas:**
- `/caminho/local/do/pacote` é o diretório local com o pacote-modelo baixado do
  Hugging Face (ver seção "Obtendo o pacote-modelo" abaixo)
- `-v ... :ro` monta em modo somente-leitura (segurança)
- Porta exposta: 8000 (ajuste com `-p HOST:8000` conforme necessário)
- O servidor está pronto quando logs mostram `Application startup complete`

---

## Obtendo o pacote-modelo

O modelo treinado é distribuído separadamente, no Hugging Face:
**https://huggingface.co/DominuZ/ncm-classificador-bertimbau** (CC BY 4.0).
O pacote de inferência são estes 5 itens do repositório: `modelo.onnx` (~1,3 GB),
`tokenizer/`, `labels.json`, `inferencia.json` e `manifest.json` — a integridade
(SHA-256 do manifesto) é conferida automaticamente antes de qualquer resposta.

```bash
pip install huggingface_hub
hf download DominuZ/ncm-classificador-bertimbau \
  modelo.onnx labels.json inferencia.json manifest.json tokenizer/tokenizer.json tokenizer/tokenizer_config.json \
  --local-dir ~/.ncm/pacote
```

Uma vez baixado, aponte a variável de ambiente `NCM_PACOTE` para o diretório
(ou use o padrão `~/.ncm/pacote`, que dispensa a variável):

```bash
export NCM_PACOTE=~/.ncm/pacote
ncm classificar "PARAF SEXT ZINC M8X40 DIN933"
ncm servir
```

Ou via Docker:
```bash
docker run -v /seu/caminho/pacote:/pacote -e NCM_PACOTE=/pacote ...
```

---

## Limitações e abstenção

O modelo abstém em casos ambíguos ou com baixa confiança na predição (atualmente ~48%
dos itens). A abstenção é **um recurso, não um defeito**: itens abstidos devem ser
revisados manualmente pela equipe de conformidade ou contador.

Exemplo real de abstenção (saída do `ncm lote`, ver seção de uso acima): o item
`PORCAS M8 ISO 4032` recebeu `abstem=true` porque o 1º candidato (`73229090`) ficou em
35,2% de confiança, abaixo do piso de 55% configurado no pacote `d-1`. Na CLI interativa
(`ncm classificar`), abstenção aparece como a linha
`ABSTEVE — confiança insuficiente; escale a um contador` logo abaixo da tabela.

---

## Links e suporte

- **Modelo (pesos + pacote ONNX):** https://huggingface.co/DominuZ/ncm-classificador-bertimbau
  — o model card documenta métricas, calibração, dados de treino e limitações
- **Benchmark público (RFB-bench):** https://huggingface.co/datasets/DominuZ/rfb-bench
- Dúvidas e problemas: use as discussões (aba *Community*) do repositório do modelo
  no Hugging Face

**Licença:** código MIT · pesos do modelo CC BY 4.0
**Versão:** 0.1.0
**Última atualização:** 2026-07-17
