Skip to main content

Classificador fiscal NCM: top-3 + confiança calibrada + abstenção, offline na sua CPU

Project description

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)

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

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):

ncm classificar "PARAF SEXT ZINC M8X40 DIN933" --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)

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

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

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:

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):

{
  "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:

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):

{
  "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:

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:

{
  "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:

curl http://localhost:8000/healthz

Response (200):

{"status": "ok"}

Docker

Build

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

# 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:

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.

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):

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

Ou via Docker:

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

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ncm_classificador-0.1.0.tar.gz (17.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ncm_classificador-0.1.0-py3-none-any.whl (20.1 kB view details)

Uploaded Python 3

File details

Details for the file ncm_classificador-0.1.0.tar.gz.

File metadata

  • Download URL: ncm_classificador-0.1.0.tar.gz
  • Upload date:
  • Size: 17.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for ncm_classificador-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f585c90f790fc331c87a8d6334bfd0a1b4f832ecabe7ab4d5df05116f2eef57c
MD5 23c7aa54d369cb0fd424216d19bdb1b4
BLAKE2b-256 b3c97e411f9b3467732df2428dbaa7bcb9c8eeb2d894ee637ce996fb1106390f

See more details on using hashes here.

File details

Details for the file ncm_classificador-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: ncm_classificador-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 20.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for ncm_classificador-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b0e9312081f08051d27aded39bb2bf689330ea29a7ba09c3dcd4e7685dfcca17
MD5 97cb3cc3c95d00b016fddc81daa56978
BLAKE2b-256 cee8d116613bb688e48ca8b83704ec5175c10bcb39b95c0fd3d9977151047f25

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page